"""
===============================================================================
RPA File Management Framework
===============================================================================

Framework unificado para manipulação segura e inteligente de arquivos
e diretórios em ambientes de automação RPA, integrando:

• pathlib (manipulação moderna de caminhos)
• shutil (operações avançadas de arquivos)
• hashlib (validação de integridade)
• zipfile (compactação e extração)
• tempfile (arquivos temporários seguros)
• logging corporativo integrado

-------------------------------------------------------------------------------
Visão Geral
-------------------------------------------------------------------------------

Este framework fornece utilitários robustos para gerenciamento de arquivos
em automações corporativas, incluindo:

- Monitoramento de diretórios
- Controle de downloads
- Versionamento automático
- Validação de integridade
- Backup e arquivamento
- Operações atômicas
- Processamento em lote
- Controle seguro de exclusão

-------------------------------------------------------------------------------
Principais Recursos
-------------------------------------------------------------------------------

✔ Criação automática de diretórios (idempotente)
✔ Espera inteligente por arquivos e desbloqueio
✔ Movimentação segura com controle de overwrite
✔ Versionamento automático de arquivos
✔ Limpeza controlada de diretórios
✔ Listagem avançada com filtros e recursividade
✔ Extração de metadados
✔ Exclusão segura (incluindo envio para lixeira)
✔ Monitoramento contínuo de diretórios
✔ Criação de arquivos temporários seguros
✔ Hash de arquivos (SHA256, MD5, etc.)
✔ Escrita atômica (evita corrupção)
✔ Renomeação com timestamp
✔ Compactação e extração ZIP
✔ Movimentação por extensão
✔ Comparação de arquivos
✔ Validação de extensão
✔ Contagem de arquivos
✔ Backup automatizado com timestamp
✔ Arquivamento de arquivos antigos

-------------------------------------------------------------------------------
Dependências
-------------------------------------------------------------------------------

Instale os pacotes necessários com:

    pip install send2trash

===============================================================================
"""

from __future__ import annotations

import filecmp
import hashlib
import os
import shutil
import tempfile
import time
import zipfile
from collections.abc import Callable, Iterable
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

import send2trash

from logger_config import logger


def ensure_directory(path: str | Path) -> Path:
    """
    Garante que um diretório exista, criando-o se necessário.

    Args:
        path (str | Path):
            Caminho do diretório a ser garantido.

    Returns:
        Path:
            Objeto Path do diretório criado ou já existente.
    """
    directory = Path(path)
    directory.mkdir(parents=True, exist_ok=True)
    logger.info(f"[ENSURE_DIRECTORY] Diretório garantido: {directory}")
    return directory


def wait_for_file(
    path: str | Path,
    timeout: int = 30,
    poll_interval: float = 1.0,
) -> Path:
    """
    Aguarda até que um arquivo exista no caminho especificado.

    Args:
        path (str | Path):
            Caminho do arquivo a ser aguardado.
        timeout (int):
            Tempo máximo de espera em segundos.
        poll_interval (float):
            Intervalo entre verificações em segundos.

    Returns:
        Path:
            Caminho do arquivo encontrado.

    Raises:
        TimeoutError:
            Caso o arquivo não seja encontrado dentro do tempo limite.
    """
    file_path = Path(path)
    start = time.time()

    while time.time() - start <= timeout:
        if file_path.exists():
            logger.info(f"[WAIT_FOR_FILE] Arquivo encontrado: {file_path}")
            return file_path
        time.sleep(poll_interval)

    raise TimeoutError(f"Arquivo não encontrado dentro de {timeout}s: {path}")


def wait_for_file_unlock(
    path: str | Path,
    timeout: int = 30,
    poll_interval: float = 1.0,
) -> None:
    """
    Aguarda até que um arquivo não esteja mais bloqueado por outro processo.

    Args:
        path (str | Path):
            Caminho do arquivo.
        timeout (int):
            Tempo máximo de espera em segundos.
        poll_interval (float):
            Intervalo entre tentativas em segundos.

    Raises:
        TimeoutError:
            Caso o arquivo permaneça bloqueado após o tempo limite.
    """
    file_path = Path(path)
    start = time.time()

    while time.time() - start <= timeout:
        try:
            with open(file_path, "a"):
                logger.info(f"[WAIT_FOR_UNLOCK] Arquivo desbloqueado: {file_path}")
                return
        except OSError:
            time.sleep(poll_interval)

    raise TimeoutError(f"Arquivo permanece bloqueado após {timeout}s: {path}")


def move_file_safe(
    src: str | Path,
    dst: str | Path,
    overwrite: bool = False,
) -> Path:
    """
    Move um arquivo de forma segura.

    Args:
        src (str | Path):
            Caminho do arquivo origem.
        dst (str | Path):
            Caminho do arquivo destino.
        overwrite (bool):
            Se True, sobrescreve o arquivo destino caso exista.

    Returns:
        Path:
            Caminho final do arquivo movido.

    Raises:
        FileNotFoundError:
            Caso o arquivo origem não exista.
        FileExistsError:
            Caso o destino exista e overwrite seja False.
    """
    src_path = Path(src)
    dst_path = Path(dst)

    if not src_path.exists():
        raise FileNotFoundError(f"Arquivo origem não existe: {src}")

    if dst_path.exists() and not overwrite:
        raise FileExistsError(f"Arquivo destino já existe: {dst}")

    if dst_path.exists():
        dst_path.unlink()

    ensure_directory(dst_path.parent)
    shutil.move(str(src_path), str(dst_path))

    logger.info(f"[MOVE_FILE] {src_path} → {dst_path}")
    return dst_path


def copy_file_with_versioning(
    src: str | Path,
    dst_folder: str | Path,
) -> Path:
    """
    Copia um arquivo para uma pasta criando versão automática se já existir.

    Args:
        src (str | Path):
            Caminho do arquivo origem.
        dst_folder (str | Path):
            Pasta destino.

    Returns:
        Path:
            Caminho final do arquivo copiado.
    """
    src_path = Path(src)
    dst_folder_path = ensure_directory(dst_folder)

    base_name = src_path.stem
    suffix = src_path.suffix
    target_path = dst_folder_path / f"{base_name}{suffix}"

    version = 2
    while target_path.exists():
        target_path = dst_folder_path / f"{base_name}_v{version}{suffix}"
        version += 1

    shutil.copy2(src_path, target_path)
    logger.info(f"[COPY_VERSIONED] {src_path} → {target_path}")
    return target_path


def clean_directory(
    path: str | Path,
    older_than_days: int | None = None,
) -> None:
    """
    Remove arquivos de uma pasta.

    Args:
        path (str | Path):
            Caminho da pasta.
        older_than_days (int | None):
            Remove apenas arquivos mais antigos que X dias.
            Se None, remove todos os arquivos.
    """
    directory = Path(path)

    if not directory.exists():
        return

    now = datetime.now()

    for file in directory.iterdir():
        if file.is_file():
            if older_than_days is not None:
                file_age = now - datetime.fromtimestamp(file.stat().st_mtime)
                if file_age < timedelta(days=older_than_days):
                    continue
            file.unlink()
            logger.info(f"[CLEAN_DIRECTORY] Removido: {file}")


def list_files(
    path: str | Path,
    pattern: str | None = None,
    recursive: bool = False,
) -> list[Path]:
    """
    Lista arquivos de um diretório com filtro opcional.

    Args:
        path (str | Path):
            Caminho do diretório.
        pattern (str | None):
            Padrão glob (ex: "*.pdf").
        recursive (bool):
            Se True, busca recursivamente.

    Returns:
        list[Path]:
            Lista de arquivos encontrados.
    """
    directory = Path(path)

    if not directory.exists():
        return []

    files = (
        directory.rglob(pattern or "*") if recursive else directory.glob(pattern or "*")
    )

    result = [f for f in files if f.is_file()]
    logger.info(f"[LIST_FILES] {len(result)} arquivo(s) encontrado(s)")
    return result


def get_file_metadata(path: str | Path) -> dict[str, Any]:
    """
    Retorna metadados básicos de um arquivo.

    Args:
        path (str | Path):
            Caminho do arquivo.

    Returns:
        dict[str, Any]:
            Dicionário com informações do arquivo.

    Raises:
        FileNotFoundError:
            Caso o arquivo não exista.
    """
    file_path = Path(path)

    if not file_path.exists():
        raise FileNotFoundError(path)

    stat = file_path.stat()

    return {
        "name": file_path.name,
        "size_bytes": stat.st_size,
        "created_at": datetime.fromtimestamp(stat.st_ctime),
        "modified_at": datetime.fromtimestamp(stat.st_mtime),
        "extension": file_path.suffix,
    }


def safe_delete(
    path: str | Path,
    send_to_trash: bool = False,
) -> None:
    """
    Remove um arquivo com segurança.

    Args:
        path (str | Path):
            Caminho do arquivo.
        send_to_trash (bool):
            Se True, envia para lixeira.
            Se False, remove permanentemente.
    """
    file_path = Path(path)

    if not file_path.exists():
        return

    if send_to_trash:

        send2trash.send2trash(str(file_path))
        logger.info(f"[SAFE_DELETE] Enviado para lixeira: {file_path}")
    else:
        file_path.unlink()
        logger.info(f"[SAFE_DELETE] Removido permanentemente: {file_path}")


def wait_for_new_file_in_directory(
    path: str | Path,
    timeout: int = 30,
    poll_interval: float = 1.0,
) -> Path:
    """
    Aguarda até que um novo arquivo seja criado em uma pasta.

    Args:
        path (str | Path):
            Caminho da pasta.
        timeout (int):
            Tempo máximo de espera em segundos.
        poll_interval (float):
            Intervalo entre verificações.

    Returns:
        Path:
            Caminho do novo arquivo detectado.

    Raises:
        TimeoutError:
            Caso nenhum novo arquivo seja detectado.
    """
    directory = Path(path)
    existing_files = set(directory.iterdir())
    start = time.time()

    while time.time() - start <= timeout:
        current_files = set(directory.iterdir())
        new_files = current_files - existing_files

        for file in new_files:
            if file.is_file():
                logger.info(f"[WAIT_NEW_FILE] Novo arquivo detectado: {file}")
                return file

        time.sleep(poll_interval)

    raise TimeoutError(f"Nenhum novo arquivo detectado em {timeout}s.")


def get_latest_file(
    path: str | Path,
    pattern: str | None = None,
) -> Path | None:
    """
    Retorna o arquivo mais recente de um diretório.

    Args:
        path (str | Path):
            Caminho do diretório.
        pattern (str | None):
            Filtro glob (ex: "*.pdf").

    Returns:
        Path | None:
            Arquivo mais recente ou None se não houver arquivos.
    """
    directory = Path(path)
    files = list(directory.glob(pattern or "*"))

    files = [f for f in files if f.is_file()]
    if not files:
        return None

    latest = max(files, key=lambda f: f.stat().st_mtime)
    logger.info(f"[LATEST_FILE] {latest}")
    return latest


def file_exists_and_not_empty(path: str | Path) -> bool:
    """
    Verifica se o arquivo existe e possui conteúdo.

    Args:
        path (str | Path):
            Caminho do arquivo.

    Returns:
        bool:
            True se existir e tamanho > 0.
    """
    file_path = Path(path)
    exists = file_path.exists() and file_path.stat().st_size > 0
    logger.info(f"[FILE_VALIDATION] {file_path} válido: {exists}")
    return exists


def calculate_file_hash(
    path: str | Path,
    algorithm: str = "sha256",
) -> str:
    """
    Calcula o hash de um arquivo.

    Args:
        path (str | Path):
            Caminho do arquivo.
        algorithm (str):
            Algoritmo (ex: sha256, md5).

    Returns:
        str:
            Hash hexadecimal.

    Raises:
        FileNotFoundError:
            Se o arquivo não existir.
    """
    file_path = Path(path)

    if not file_path.exists():
        raise FileNotFoundError(path)

    hasher = hashlib.new(algorithm)

    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hasher.update(chunk)

    result = hasher.hexdigest()
    logger.info(f"[FILE_HASH] {file_path} → {result}")
    return result


def atomic_write(path: str | Path, content: str) -> None:
    """
    Escreve conteúdo em arquivo de forma atômica.

    Args:
        path (str | Path):
            Caminho do arquivo.
        content (str):
            Conteúdo a ser escrito.
    """
    file_path = Path(path)
    temp_path = file_path.with_suffix(".tmp")

    with open(temp_path, "w", encoding="utf-8") as f:
        f.write(content)

    temp_path.replace(file_path)
    logger.info(f"[ATOMIC_WRITE] Escrita segura em {file_path}")


def rename_with_timestamp(path: str | Path) -> Path:
    """
    Renomeia arquivo adicionando timestamp.

    Args:
        path (str | Path):
            Caminho do arquivo.

    Returns:
        Path:
            Novo caminho do arquivo.
    """
    file_path = Path(path)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    new_name = f"{file_path.stem}_{timestamp}{file_path.suffix}"
    new_path = file_path.with_name(new_name)

    file_path.rename(new_path)
    logger.info(f"[RENAME_TIMESTAMP] {new_path}")
    return new_path


def zip_directory(
    path: str | Path,
    output_path: str | Path | None = None,
) -> Path:
    """
    Compacta um diretório em ZIP.

    Args:
        path (str | Path):
            Diretório a ser compactado.
        output_path (str | Path | None):
            Caminho do zip final.

    Returns:
        Path:
            Caminho do arquivo zip gerado.
    """
    directory = Path(path)
    output_path = Path(output_path or f"{directory}.zip")

    with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zipf:
        for file in directory.rglob("*"):
            if file.is_file():
                zipf.write(file, file.relative_to(directory))

    logger.info(f"[ZIP_DIRECTORY] Criado: {output_path}")
    return output_path


def unzip_file(
    zip_path: str | Path,
    extract_to: str | Path,
) -> None:
    """
    Extrai um arquivo ZIP.

    Args:
        zip_path (str | Path):
            Caminho do zip.
        extract_to (str | Path):
            Pasta destino.
    """
    zip_path = Path(zip_path)
    extract_to = Path(extract_to)

    with zipfile.ZipFile(zip_path, "r") as zipf:
        zipf.extractall(extract_to)

    logger.info(f"[UNZIP] Extraído para {extract_to}")


def move_files_by_extension(
    src_folder: str | Path,
    dst_folder: str | Path,
    extension: str,
) -> None:
    """
    Move arquivos por extensão.

    Args:
        src_folder (str | Path):
            Pasta origem.
        dst_folder (str | Path):
            Pasta destino.
        extension (str):
            Extensão (ex: ".pdf").
    """
    src = Path(src_folder)
    dst = Path(dst_folder)
    dst.mkdir(parents=True, exist_ok=True)

    for file in src.glob(f"*{extension}"):
        shutil.move(str(file), dst / file.name)
        logger.info(f"[MOVE_BY_EXT] {file} → {dst}")


def monitor_directory(
    path: str | Path,
    callback: Callable[[Path], None],
    interval: float = 5.0,
) -> None:
    """
    Monitora diretório e executa callback quando novo arquivo surge.

    Args:
        path (str | Path):
            Diretório monitorado.
        callback (Callable):
            Função executada ao detectar novo arquivo.
        interval (float):
            Intervalo de verificação em segundos.
    """
    directory = Path(path)
    known_files = set(directory.iterdir())

    logger.info(f"[MONITOR] Monitorando {directory}")

    while True:
        current_files = set(directory.iterdir())
        new_files = current_files - known_files

        for file in new_files:
            if file.is_file():
                logger.info(f"[MONITOR] Novo arquivo: {file}")
                callback(file)

        known_files = current_files
        time.sleep(interval)


def create_temp_file(
    prefix: str | None = None,
    suffix: str | None = None,
) -> Path:
    """
    Cria um arquivo temporário seguro.

    Args:
        prefix (str | None):
            Prefixo do nome.
        suffix (str | None):
            Sufixo/extensão.

    Returns:
        Path:
            Caminho do arquivo temporário criado.
    """
    fd, temp_path = tempfile.mkstemp(prefix=prefix or "", suffix=suffix or "")
    os.close(fd)

    temp_file = Path(temp_path)
    logger.info(f"[TEMP_FILE] Criado: {temp_file}")
    return temp_file


def compare_files(
    file1: str | Path,
    file2: str | Path,
    shallow: bool = False,
) -> bool:
    """
    Compara dois arquivos.

    Args:
        file1 (str | Path):
            Caminho do primeiro arquivo.
        file2 (str | Path):
            Caminho do segundo arquivo.
        shallow (bool):
            Se True, compara apenas metadados.
            Se False, compara conteúdo.

    Returns:
        bool:
            True se os arquivos forem iguais.
    """
    result = filecmp.cmp(str(file1), str(file2), shallow=shallow)
    logger.info(f"[COMPARE_FILES] {file1} == {file2}: {result}")
    return result


def validate_file_extension(
    path: str | Path,
    allowed_extensions: Iterable[str],
) -> bool:
    """
    Valida se a extensão do arquivo é permitida.

    Args:
        path (str | Path):
            Caminho do arquivo.
        allowed_extensions (Iterable[str]):
            Lista de extensões permitidas (ex: [".pdf", ".xml"]).

    Returns:
        bool:
            True se a extensão for válida.
    """
    file_path = Path(path)
    is_valid = file_path.suffix.lower() in {ext.lower() for ext in allowed_extensions}

    logger.info(f"[VALIDATE_EXTENSION] {file_path} válido: {is_valid}")
    return is_valid


def count_files(
    path: str | Path,
    pattern: str | None = None,
    recursive: bool = False,
) -> int:
    """
    Conta arquivos em um diretório.

    Args:
        path (str | Path):
            Caminho do diretório.
        pattern (str | None):
            Filtro glob (ex: "*.pdf").
        recursive (bool):
            Se True, busca recursivamente.

    Returns:
        int:
            Quantidade de arquivos encontrados.
    """
    directory = Path(path)

    files = (
        directory.rglob(pattern or "*") if recursive else directory.glob(pattern or "*")
    )

    total = sum(1 for f in files if f.is_file())
    logger.info(f"[COUNT_FILES] {directory}: {total} arquivo(s)")
    return total


def backup_file(
    path: str | Path,
    backup_folder: str | Path,
) -> Path:
    """
    Cria backup de um arquivo adicionando timestamp.

    Args:
        path (str | Path):
            Caminho do arquivo original.
        backup_folder (str | Path):
            Pasta onde o backup será salvo.

    Returns:
        Path:
            Caminho do arquivo de backup.
    """
    file_path = Path(path)
    backup_dir = Path(backup_folder)
    backup_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"{file_path.stem}_backup_{timestamp}{file_path.suffix}"
    backup_path = backup_dir / backup_name

    shutil.copy2(file_path, backup_path)

    logger.info(f"[BACKUP_FILE] Backup criado: {backup_path}")
    return backup_path


def archive_old_files(
    source_folder: str | Path,
    archive_folder: str | Path,
    older_than_days: int,
) -> None:
    """
    Move arquivos antigos para pasta de arquivamento.

    Args:
        source_folder (str | Path):
            Pasta origem.
        archive_folder (str | Path):
            Pasta destino.
        older_than_days (int):
            Arquivos mais antigos que X dias serão movidos.
    """
    source = Path(source_folder)
    archive = Path(archive_folder)
    archive.mkdir(parents=True, exist_ok=True)

    cutoff_date = datetime.now().timestamp() - (older_than_days * 86400)

    for file in source.iterdir():
        if file.is_file() and file.stat().st_mtime < cutoff_date:
            shutil.move(str(file), archive / file.name)
            logger.info(f"[ARCHIVE] {file} → {archive}")
