"""
OneromExecution — ponto de entrada principal do SDK ONEROM.

Uso basico:
    from onerom_core import OneromExecution
    import logging

    exec = OneromExecution()
    logging.info("Iniciando bot")   # capturado pelo runner via stdout JSON

    nome = exec.params.nome         # parametro tipado da execucao

    exec.add_success_item()
    exec.finish_success()

Uso com wrapper automatico:
    exec.run(lambda: my_main_function())

Uso com logger customizado:
    exec.attach_logger(logging.getLogger("meu_modulo"))
"""
import json
import logging
import os
import traceback as tb
from pathlib import Path
from typing import Any, Callable, Optional

from .client import OneromClient
from .logging_handler import OneromLogHandler
from .params import build_params

logger = logging.getLogger(__name__)


class OneromExecution:
    """
    Contexto de execucao ONEROM.

    Gerencia:
    - Parametros tipados e validados
    - Logging estruturado via stdout (capturado pelo runner)
    - Metricas de itens processados via arquivo (lido pelo runner)
    - Ciclo de vida da execucao via HTTP (quando disponivel)

    Design:
    - Nunca lanca excecao em metodos de ciclo de vida (mark_running, finish_*)
    - Erros de parametro sao imediatos e claros
    - Compativel com Python 3.9+
    """

    def __init__(self) -> None:
        """
        Inicializa o contexto de execucao.

        Raises:
            RuntimeError: Se ONEROM_EXECUTION_ID nao estiver definido.
            ValueError: Se parametros obrigatorios estiverem ausentes ou invalidos.
        """
        # Valida ambiente ONEROM antes de qualquer coisa
        if "ONEROM_EXECUTION_ID" not in os.environ:
            raise RuntimeError(
                "ONEROM_EXECUTION_ID nao definido.\n"
                "Este SDK deve ser executado dentro do ONEROM Runner.\n"
                "Variaveis necessarias: ONEROM_EXECUTION_ID, ONEROM_BOT_NAME, "
                "ONEROM_RUNNER_ID, ONEROM_PARAMETERS"
            )

        # Cliente HTTP (modo passivo se API_URL/RUNNER_KEY ausentes)
        self._client = OneromClient()

        # Arquivo de metricas (runner le apos fim da execucao)
        metrics_path = os.environ.get("ONEROM_METRICS_FILE", "")
        self._metrics_file: Optional[Path] = Path(metrics_path) if metrics_path else None

        # Contadores locais de itens
        self._items_processed: int = 0
        self._items_failed: int = 0

        # Parametros da execucao (validados em tempo de instanciacao)
        self._params: Any = build_params()

        # Anexa handler de log estruturado ao logger raiz automaticamente
        root_logger = logging.getLogger()
        # Evita duplicar o handler se OneromExecution for instanciado mais de uma vez
        if not any(isinstance(h, OneromLogHandler) for h in root_logger.handlers):
            root_logger.addHandler(OneromLogHandler())
        # Garante que o root logger repasse todos os níveis ao handler
        # (padrão Python é WARNING, o que silencia INFO/DEBUG de módulos externos)
        if root_logger.level == logging.WARNING or root_logger.level == 0:
            root_logger.setLevel(logging.DEBUG)

        logger.debug(
            f"OneromExecution inicializado "
            f"(execution_id={self._client.execution_id}, "
            f"http_available={self._client.available})"
        )

    # ── Parametros ──────────────────────────────────────────────────────────

    @property
    def params(self) -> Any:
        """
        Parametros da execucao como objeto com acesso por atributo.

        Se ONEROM_PARAM_SCHEMA estiver definido: Pydantic model validado.
        Caso contrario: SimpleNamespace com valores brutos.

        Exemplo:
            nome = exec.params.nome
            count = exec.params.count
        """
        return self._params

    # ── Logging ─────────────────────────────────────────────────────────────

    def attach_logger(self, log: logging.Logger) -> None:
        """
        Anexa o handler ONEROM a um logger especifico.

        O logger raiz ja recebe o handler automaticamente no __init__.
        Use este metodo apenas se precisar de um nivel customizado em um logger nomeado.

        Nota: desativa propagate no logger fornecido para evitar que cada mensagem
        seja enviada em duplicata (pelo handler do logger nomeado e pelo root logger).

        Args:
            log: Logger ao qual o handler sera adicionado.
        """
        if not any(isinstance(h, OneromLogHandler) for h in log.handlers):
            log.addHandler(OneromLogHandler())
        # Evita duplicacao: o root logger ja captura via propagacao por padrao
        log.propagate = False
        # Garante que o logger nomeado repasse todos os níveis ao handler
        # (sem isso, NOTSET herdaria do pai, mas com propagate=False o pai é ignorado)
        if log.level == logging.NOTSET:
            log.setLevel(logging.DEBUG)

    # ── Ciclo de vida ────────────────────────────────────────────────────────

    def mark_running(self) -> None:
        """
        Notifica o backend que a execucao iniciou.
        O runner ja reporta RUNNING antes de spawnar o subprocess —
        use este metodo para reconfirmar ou apos delay no startup.
        """
        self._client.put_execution("RUNNING")

    def report_progress(self, processed: int, total: int) -> None:
        """
        Reporta progresso parcial da execucao.

        Atualiza o arquivo de metricas (lido pelo runner a cada 1s).

        Args:
            processed: Numero de itens processados ate agora.
            total: Total de itens esperados (ignorado — reservado para uso futuro).
        """
        self._items_processed = processed
        self._write_metrics()

    def add_success_item(self) -> None:
        """Incrementa contador de itens processados com sucesso e persiste métricas."""
        self._items_processed += 1
        self._write_metrics()

    def add_failed_item(self) -> None:
        """Incrementa contador de itens que falharam e persiste métricas."""
        self._items_failed += 1
        self._write_metrics()

    def finish_success(self) -> None:
        """
        Finaliza a execucao como bem-sucedida.

        1. Persiste metricas no arquivo (runner le ao encerrar)
        2. Envia status SUCCESS via HTTP (se disponivel)
        """
        self._write_metrics()
        self._client.put_execution(
            "SUCCESS",
            items_processed=self._items_processed,
            items_failed=self._items_failed,
        )

    def finish_failed(self, error_message: str) -> None:
        """
        Finaliza a execucao como falha.

        1. Persiste metricas no arquivo
        2. Envia status FAILED com mensagem e traceback via HTTP (se disponivel)

        Args:
            error_message: Descricao do erro que causou a falha.
        """
        self._write_metrics()
        self._client.put_execution(
            "FAILED",
            error_message=error_message,
            error_traceback=tb.format_exc(),
            items_processed=self._items_processed,
            items_failed=self._items_failed,
        )

    # ── Wrapper automatico ───────────────────────────────────────────────────

    def run(self, fn: Callable[[], None]) -> None:
        """
        Executa fn() gerenciando o ciclo de vida automaticamente.

        Sequencia:
            mark_running() → fn() → finish_success()
        Em caso de excecao:
            finish_failed(str(e)) → re-raise

        Args:
            fn: Funcao principal do bot (sem argumentos).

        Raises:
            Exception: Propaga qualquer excecao lancada por fn().
        """
        try:
            self.mark_running()
            fn()
            self.finish_success()
        except Exception as exc:
            self.finish_failed(str(exc))
            raise

    # ── Internos ─────────────────────────────────────────────────────────────

    def _write_metrics(self) -> None:
        """Escreve metricas no arquivo lido pelo runner apos o subprocess encerrar."""
        if not self._metrics_file:
            return
        try:
            self._metrics_file.parent.mkdir(parents=True, exist_ok=True)
            self._metrics_file.write_text(
                json.dumps({
                    "items_processed": self._items_processed,
                    "items_failed": self._items_failed,
                }),
                encoding="utf-8",
            )
        except Exception as exc:
            logger.debug(f"Falha ao escrever metrics file: {exc}")
