"""
===============================================================================
File Lock Manager
===============================================================================

Biblioteca para controle de execução única utilizando arquivos de lock.

Permite impedir que múltiplas instâncias de um mesmo processo sejam
executadas simultaneamente na mesma máquina.

Recursos principais:

• Criação atômica de arquivo de lock
• Controle por PID e hostname
• Detecção de lock órfão
• Timeout configurável
• Remoção forçada (force unlock)
• Suporte a context manager (with)
• Logger integrado
• Modo debug (não cria arquivo)

Dependências:

    pip install psutil

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

from __future__ import annotations

import json
import os
import socket
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path

import psutil

from logger_config import logger


class LockAcquisitionError(Exception):
    """Lançada quando não é possível adquirir o lock."""


@dataclass
class LockConfig:
    directory: str | None = None
    timeout_seconds: int | None = None
    auto_cleanup: bool = True
    raise_on_locked: bool = True
    debug: bool = False


class FileLock:
    """
    Implementação robusta de File Lock baseada em arquivo.

    Recursos:
        - Criação atômica
        - PID tracking
        - Hostname tracking
        - Timeout opcional
        - Verificação de processo vivo
        - Detecção de lock órfão
        - Context manager
        - Debug mode (não cria arquivo)
    """

    def __init__(self, name: str, config: LockConfig | None = None):
        self.name = name
        self.config = config or LockConfig()
        self.hostname = socket.gethostname()
        self.pid = os.getpid()

        self.lock_dir = Path(self.config.directory or Path.cwd() / "locks")
        self.lock_dir.mkdir(parents=True, exist_ok=True)

        self.lock_path = self.lock_dir / f"{self.name}.lock"

    def acquire(self) -> bool:
        """
        Tenta adquirir o lock.

        Returns:
            True se lock adquirido.
            False se já estiver bloqueado e raise_on_locked=False.

        Raises:
            LockAcquisitionError se não conseguir adquirir e raise_on_locked=True.
        """

        if self.config.debug:
            logger.warning("[DEBUG MODE] Lock não será criado.")
            return True

        if self.lock_path.exists():
            logger.warning("Lock existente detectado.")
            if self._is_stale():
                logger.warning("Lock órfão detectado. Removendo...")
                self.release(force=True)
            else:
                return self._handle_locked()

        try:
            fd = os.open(
                self.lock_path,
                os.O_CREAT | os.O_EXCL | os.O_WRONLY,
            )

            metadata = self._build_metadata()

            with os.fdopen(fd, "w") as f:
                json.dump(metadata, f)

            logger.info(f"Lock adquirido: {self.lock_path}")
            return True

        except FileExistsError:
            return self._handle_locked()

    def release(self, force: bool = False) -> None:
        """
        Remove o lock.

        Args:
            force: Se True, remove independentemente do PID.
        """

        if self.config.debug:
            logger.warning("[DEBUG MODE] Lock não será removido.")
            return

        if not self.lock_path.exists():
            return

        if not force:
            metadata = self._read_metadata()
            if metadata and metadata.get("pid") != self.pid:
                logger.warning("Tentativa de remover lock de outro processo.")
                return

        try:
            self.lock_path.unlink()
            logger.info(f"Lock removido: {self.lock_path}")
        except Exception as e:
            logger.error(f"Erro ao remover lock: {e}")

    def is_locked(self) -> bool:
        return self.lock_path.exists()

    def __enter__(self) -> FileLock:
        acquired = self.acquire()
        if not acquired:
            raise LockAcquisitionError(
                f"Não foi possível adquirir lock: {self.lock_path}"
            )
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.release()

    def _handle_locked(self) -> bool:
        message = f"Processo já em execução: {self.lock_path}"

        if self.config.raise_on_locked:
            logger.error(message)
            raise LockAcquisitionError(message)

        logger.warning(message)
        return False

    def _build_metadata(self) -> dict:
        return {
            "pid": self.pid,
            "hostname": self.hostname,
            "created_at": datetime.now(UTC).isoformat(),
        }

    def _read_metadata(self) -> dict | None:
        try:
            with open(self.lock_path) as f:
                return json.load(f)
        except Exception:
            return None

    def _is_process_alive(self, pid: int) -> bool:
        return psutil.pid_exists(pid)

    def _is_stale(self) -> bool:
        """
        Determina se o lock está órfão ou expirado.
        """

        metadata = self._read_metadata()
        if not metadata:
            return True

        pid = metadata.get("pid")
        created_at = metadata.get("created_at")

        # PID morto → stale
        if not pid or not self._is_process_alive(pid):
            return True

        # Timeout configurado
        if self.config.timeout_seconds and created_at:
            created_time = datetime.fromisoformat(created_at)
            if datetime.utcnow() - created_time > timedelta(
                seconds=self.config.timeout_seconds
            ):
                return True

        return False
