#!/usr/bin/env python3
from __future__ import annotations
from typing import Union
from organize_stream.find import DocumentFinder
from organize_stream.read import (
    read_file_pdf, read_document_pdf, read_image, create_table_from_dict
)
import soup_files as sp
import convert_stream as cs
import pandas as pd
import shutil

FindItem = Union[str, list[str]]

#  
_bad_chars: list[str] = [
    '.', '!', ':', '?', '(', ')', '{', '}',
    '+', '#', '@', '<', '>', '/', '¢', ':',
    ',', '®', '“', "“", ';', '‘',
]

_remove_end_name: list[str] = ['-']
_remove_start_name: list[str] = ['-']


def fmt_str_file(
            filename: str, *,
            max_char: int = 80,
            upper_case: bool = True
        ) -> str:
    for c in _bad_chars:
        filename = filename.replace(c, '')
    for c in _remove_end_name:
        if filename[-1] == c:
            filename = filename[:-1]
    for c in _remove_start_name:
        while c in filename[0]:
            filename = filename[1:]
    while '--' in filename:
        filename = filename.replace('--', '-')
    if upper_case:
        filename = filename.upper()
    if len(filename) <= max_char:
        return filename
    return filename[0:max_char]


def move_list_files(
            mv_items: dict[str, list[sp.File]], *,
            replace: bool = False
        ) -> None:
    total_file = len(mv_items['src'])
    for idx, file in enumerate(mv_items['src']):
        output_path: sp.File = mv_items['dest'][idx]
        if not file.exists():
            print(f'[PULANDO]: {idx + 1} Arquivo não encontrado {file.absolute()}')
        if output_path.exists():
            if not replace:
                _count = 0
                origin_name = output_path.name_absolute()
                origin_ext = output_path.extension()
                while output_path.exists():
                    _count += 1
                    new_name = f'{origin_name}-{_count}{origin_ext}'
                    output_path = sp.File(new_name)
                del origin_name
                del origin_ext
        print(f'Movendo: {idx+1}/{total_file} {file.absolute()}')
        try:
            shutil.move(file.absolute(), output_path.absolute())
        except Exception as e:
            print(f'{e}')
        del output_path


# Sujeito notificador
class NotifyProvider(object):

    def __init__(self):
        self.observers: list = []

    def add_observer(self, observer) -> None:
        self.observers.append(observer)

    def send_notify(self, tb: cs.TextTable) -> None:
        for obs in self.observers:
            obs.receive_notify(tb)


# Sujeito Observador.
class Observer(object):

    def __init__(self):
        pass

    def receive_notify(self, notify: cs.TextTable) -> None:
        pass


class Filter(object):

    def __init__(
                self,
                find_txt: str,
                out_dir: sp.Directory, *,
                separator: str = ' ',
                case: bool = False,
                iqual: bool = False
            ):
        self.find_txt: str = find_txt
        self.out_dir: sp.Directory = out_dir
        self.case: bool = case
        self.iqual: bool = iqual
        self.separator: str = separator


class FilterData(Filter):

    def __init__(
                self,
                out_dir: sp.Directory, *,
                col_find: str,
                col_new_name: str,
                cols_in_name: list[str] = [],
                separator: str = ' ',
                case: bool = False,
                iqual: bool = False):
        super().__init__('nan', out_dir, separator=separator, case=case, iqual=iqual)
        self.col_find: str = col_find
        self.col_new_name: str = col_new_name
        self.cols_in_name: list[str] = cols_in_name


class DocumentTextExtract(NotifyProvider):
    """
        Extrair texto de arquivos, e converter em Excel/DataFrame
    """

    def __init__(self):
        super().__init__()
        self.tb_list: list[cs.TextTable] = []
        self.pbar: sp.ProgressBarAdapter = sp.ProgressBarAdapter()
        self.__count: int = 0

    @property
    def is_empty(self) -> bool:
        return len(self.tb_list) == 0

    @property
    def finder(self) -> DocumentFinder:
        return DocumentFinder(self.tb_list)

    def add_table(self, tb: cs.TextTable) -> None:
        self.tb_list.append(tb)
        self.__count += 1
        self.pbar.update_text(f'{__class__.__name__} Tabela adicionada: {self.__count}')
        self.send_notify(tb)

    def add_directory_pdf(
                self, 
                dir_pdf: sp.Directory, *, 
                apply_ocr: bool = False, 
                dpi: int = 200
            ):
        files = sp.InputFiles(dir_pdf).get_files(file_type=sp.LibraryDocs.PDF)
        total = len(files)
        for n, f in enumerate(files):
            self.pbar.update(
                ((n + 1) / total) * 100,
                f'{n+1}/{total} {f.basename()}',
            )
            tb = read_file_pdf(f, apply_ocr=apply_ocr, pbar=self.pbar, dpi=dpi)
            self.add_table(tb)

    def add_directory_image(self, dir_image: sp.Directory):
        files_images = sp.InputFiles(dir_image).get_files(file_type=sp.LibraryDocs.IMAGE)
        total = len(files_images)
        for idx, f in enumerate(files_images):
            self.pbar.update(
                ((idx + 1) / total) * 100,
                f'{idx+1}/{total} {f.basename()}',
            )
            tb: cs.TextTable = create_table_from_dict(read_image(f))
            self.add_table(tb)

    def add_file_pdf(self, file_pdf: sp.File, apply_ocr: bool = False):
        tb = read_file_pdf(file_pdf, pbar=self.pbar, apply_ocr=apply_ocr)
        self.add_table(tb)

    def add_file_image(self, file_image: sp.File):
        tb: cs.TextTable = create_table_from_dict(read_image(file_image))
        self.add_table(create_table_from_dict(tb))

    def add_image(self, image: cs.ImageObject):
        if not isinstance(image, cs.ImageObject):
            raise TypeError('Image must be an cs.ImageObject')
        _tb: cs.TextTable = create_table_from_dict(read_image(image))
        self.add_table(_tb)

    def add_document(
                self, document:
                cs.DocumentPdf, *,
                apply_ocr: bool = False,
                dpi: int = 200,
            ):
        self.add_table(
            read_document_pdf(
                document,
                document.metadata.file_path,
                pbar=self.pbar,
                apply_ocr=apply_ocr,
                dpi=dpi
            )
        )

    def to_data(self) -> pd.DataFrame:
        if len(self.tb_list) == 0:
            return cs.mod_types.table_types.create_void_df_map()
        
        _data: list[pd.DataFrame] = []
        for m in self.tb_list:
            _data.append(pd.DataFrame.from_dict(m))
        return pd.concat(_data).astype('str')
       
    def to_excel(self, file: sp.File) -> None:
        self.to_data().to_excel(file.absolute(), index=False)


class Organize(Observer):

    def __init__(self, filter_text: Filter):
        super().__init__()
        self._count: int = 0
        self.filter: Filter = filter_text
        self.extractor: DocumentTextExtract = DocumentTextExtract()
        self.extractor.add_observer(self)
        self.pbar: sp.ProgressBarAdapter = sp.ProgressBarAdapter()

    def _show_error(self, txt: str):
        print()
        self.pbar.update_text(f'{__class__.__name__} txt')

    def add_image(self, image: cs.ImageObject | sp.File):
        if isinstance(image, sp.File):
            self.extractor.add_file_image(image)
        elif isinstance(image, cs.ImageObject):
            self.extractor.add_image(image)
        else:
            self._show_error(f'Image must be an cs.ImageObject | sp.File')

    def add_images(self, images: list[cs.ImageObject] | list[sp.File]):
        total = len(images)
        for n, image in enumerate(images):
            if isinstance(image, sp.File):
                image = cs.ImageObject(image)
            self.pbar.update(
                ((n + 1) / total) * 100,
                f'[ADICIONANDO IMAGEM] {n+1}/{total} {image.metadata.name}'
            )
            self.add_image(image)

    def add_document(
                self,
                document: cs.DocumentPdf, *,
                apply_ocr: bool = True,
                dpi: int = 200
            ):
        self.extractor.add_document(document, apply_ocr=apply_ocr, dpi=dpi)

    def add_dir_pdf(
                self,
                path: sp.Directory, *,
                apply_ocr: bool = True,
                dpi: int = 200
            ):
        self.extractor.add_directory_pdf(path, apply_ocr=apply_ocr, dpi=dpi)

    def add_dir_image(self, path: sp.Directory):
        self.extractor.add_directory_image(path)

    def receive_notify(self, notify: cs.TextTable) -> None:
        pass


class OrganizeInnerText(Organize):
    """
    Mover/Renomear arquivos de acordo com padrões de texto presentes
    nos documentos/imagens.

    O padrão de texto a ser filtrado deve ser criado no objeto FilterText(). Se desejar
    filtrar mais de uma ocorrência nos documentos/imagens, separe as ocorrências com um '|'

    """

    def __init__(self, filter_text: Filter):
        super().__init__(filter_text)

    def receive_notify(self, notify: cs.TextTable) -> None:
        self._count += 1
        self.move_where_contains_text(notify)

    def move_where_contains_text(self, tb: cs.TextTable) -> None:
        """
        Mover/Renomear arquivos de acordo com padrões de texto presentes
        nos documentos/imagens.
        """
        data = pd.DataFrame.from_dict(tb)
        df = data[[cs.ColumnsTable.TEXT.value, cs.ColumnsTable.FILE_PATH.value]].astype('str')
        mv_items: dict[str, list[sp.File]] = {'src': [], 'dest': []}

        # Divide padrões múltiplos separados por "|"
        patterns = [p.strip() for p in self.filter.find_txt.split('|') if p.strip()]
        if not patterns:
            print(f'{__class__.__name__}: Nenhum padrão de busca válido informado.')
            return

        # Define padrão regex dependendo de "iqual"
        if self.filter.iqual:
            regex_pattern = '^(' + '|'.join(patterns) + ')$'
        else:
            regex_pattern = '(' + '|'.join(patterns) + ')'

        # Filtra linhas no DataFrame
        mask: pd.Series = df[cs.ColumnsTable.TEXT.value].str.contains(
            regex_pattern,
            case=self.filter.case,
            regex=True,
            na=False
        )
        matched_df = df[mask]
        total_matches = len(matched_df)

        if total_matches == 0:
            print(f'{__class__.__name__}: Nenhum arquivo encontrado com os padrões "{self.filter.find_txt}".')
            return
        print(f'{__class__.__name__}: {total_matches} ocorrência(s) encontrada(s).')

        # Para cada linha encontrada, gera nome limpo e adiciona à lista de movimentação
        for _, row in matched_df.iterrows():
            src_file = sp.File(row[cs.ColumnsTable.FILE_PATH.value])
            text_content = row[cs.ColumnsTable.TEXT.value]
            # Usa o texto da linha como base do novo nome
            new_file_name: str = fmt_str_file(text_content.strip())
            # Garante que o nome não fique vazio
            if not new_file_name:
                new_file_name = src_file.basename()
            dest_file = self.filter.out_dir.join_file(f"{new_file_name}{src_file.extension()}")
            mv_items['src'].append(src_file)
            mv_items['dest'].append(dest_file)
        move_list_files(mv_items)


class OrganizeInnerData(Organize):
    """
        Organizar os arquivos com base nos dados de uma tabela/DataFrame
    """

    def __init__(self, df: pd.DataFrame, filter_data: FilterData):
        """
        :param df: DataFrame com os dados de uma tabela/DataFrame, onde cada linha de determinada
        coluna será comparada com os textos presentes nos documentos.

        :param filter_data: FilterData com os nomes das colunas onde será buscado os textos a serem
        filtrados linha a linha.
        """
        super().__init__(filter_data)
        self.df: pd.DataFrame = df
        self.filter_data: FilterData = filter_data

    def receive_notify(self, notify: cs.TextTable) -> None:
        self._count += 1
        self.move_where_math_column(notify)

    def move_where_math_column(self, tb: cs.TextTable) -> None:
        """
            Mover arquivos conforme as ocorrências de texto encontradas na tabela/DataFrame df.
        o nome do novo arquivo será igual à ocorrência de texto da coluna 'col_find', podendo
        estender o nome com elementos de outras colunas, tais colunas podem ser informadas (opcionalmente)
        no parâmetro cols_in_name.
            Ex:
        Suponha que a tabela para renomear aquivos tenha a seguinte estrutura:

        A      B        C
        maça   Cidade 1 xxyyy
        banana Cidade 2 yyxxx
        mamão  Cidade 3 xyxyx

        Se passarmos os parâmetros col_find='A' e col_new_name='A' e o texto banana for
        encontrado no(s) documento, o novo nome do arquivo será banana. Caso incluir o parâmetro
        cols_in_name=['B'] o novo nome do arquivo será banana-Cidade 2 ou
        banana-Cidade 2-yyxxx (se incluir cols_in_name=['B', 'C']).

        """
        list_values_find = self.df[self.filter_data.col_find].astype('str').values.tolist()
        list_values_new_name = self.df[self.filter_data.col_new_name].astype('str').values.tolist()
        tb = pd.DataFrame.from_dict(tb)
        text_in_docs = tb[cs.ColumnsTable.TEXT.value].astype('str').values.tolist()
        text_file_names = tb[cs.ColumnsTable.FILE_PATH.value].astype('str').values.tolist()
        mv_items: dict[str, list[sp.File]] = {'src': [], 'dest': []}

        cols_include_names: list[list[str]] = []
        if len(self.filter_data.cols_in_name) > 0:
            for c in self.filter_data.cols_in_name:
                values_include: list[str] = self.df[c].astype('str').values.tolist()
                cols_include_names.append(values_include)

        for num, txt in enumerate(list_values_find):
            for num_idx, txt_doc in enumerate(text_in_docs):
                if txt in txt_doc:
                    src_file = sp.File(text_file_names[num_idx])
                    output_file = list_values_new_name[num]
                    if len(self.filter_data.cols_in_name) > 0:
                        new = ''
                        for element in cols_include_names:
                            new = f'{new}-{element[num]}'
                        output_file = f'{output_file}-{new}'
                    output_file = fmt_str_file(output_file)
                    output_path = self.filter_data.out_dir.join_file(f'{output_file}{src_file.extension()}')
                    mv_items['src'].append(src_file)
                    mv_items['dest'].append(output_path)
        move_list_files(mv_items)
