"""Tools utilitarias: calculadora juridica e data/hora."""

import ast
import math
import operator
from datetime import date, datetime, timedelta
from typing import Any, Literal
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from fastmcp import FastMCP
from fastmcp.tools.tool import ToolAnnotations

from tools._helpers import erro_instrutivo

# --- Helpers privados ---

_DIAS_SEMANA = {
    0: "segunda-feira",
    1: "terca-feira",
    2: "quarta-feira",
    3: "quinta-feira",
    4: "sexta-feira",
    5: "sabado",
    6: "domingo",
}


def _dia_semana_ptbr(d: date) -> str:
    return _DIAS_SEMANA[d.weekday()]


def _pascoa(ano: int) -> date:
    """Algoritmo de Meeus/Anonymous Gregorian para calcular Pascoa."""
    a = ano % 19
    b, c = divmod(ano, 100)
    d, e = divmod(b, 4)
    f = (b + 8) // 25
    g = (b - f + 1) // 3
    h = (19 * a + b - d - g + 15) % 30
    i, k = divmod(c, 4)
    l = (32 + 2 * e + 2 * i - h - k) % 7  # noqa: E741
    m = (a + 11 * h + 22 * l) // 451
    mes, dia = divmod(h + l - 7 * m + 114, 31)
    return date(ano, mes, dia + 1)


def _feriados_nacionais(ano: int) -> set[date]:
    """Feriados nacionais brasileiros (fixos + moveis)."""
    p = _pascoa(ano)
    return {
        # Fixos
        date(ano, 1, 1),  # Confraternizacao Universal
        date(ano, 4, 21),  # Tiradentes
        date(ano, 5, 1),  # Dia do Trabalho
        date(ano, 9, 7),  # Independencia
        date(ano, 10, 12),  # Nossa Sra Aparecida
        date(ano, 11, 2),  # Finados
        date(ano, 11, 15),  # Proclamacao da Republica
        date(ano, 12, 25),  # Natal
        # Moveis (baseados na Pascoa)
        p - timedelta(days=48),  # Segunda de Carnaval
        p - timedelta(days=47),  # Terca de Carnaval
        p - timedelta(days=2),  # Sexta-feira Santa
        p + timedelta(days=60),  # Corpus Christi
    }


def _eh_dia_util(d: date, feriados: set[date]) -> bool:
    return d.weekday() < 5 and d not in feriados


def _proximo_dia_util(d: date, feriados: set[date]) -> date:
    while not _eh_dia_util(d, feriados):
        d += timedelta(days=1)
    return d


def _em_recesso_forense(d: date) -> bool:
    """CPC art. 220: recesso de 20/dez a 20/jan (inclusive)."""
    return (d.month == 12 and d.day >= 20) or (d.month == 1 and d.day <= 20)


def _feriados_no_periodo(inicio: date, fim: date, feriados: set[date]) -> list[str]:
    """Lista feriados que caem no periodo [inicio, fim], formatados como 'AAAA-MM-DD (nome)'."""
    nomes = {
        (1, 1): "Confraternizacao Universal",
        (4, 21): "Tiradentes",
        (5, 1): "Dia do Trabalho",
        (9, 7): "Independencia",
        (10, 12): "Nossa Sra Aparecida",
        (11, 2): "Finados",
        (11, 15): "Proclamacao da Republica",
        (12, 25): "Natal",
    }
    resultado = []
    for f in sorted(feriados):
        if inicio <= f <= fim:
            nome = nomes.get((f.month, f.day), "Feriado movel")
            resultado.append(f"{f.isoformat()} ({nome})")
    return resultado


def _prazo_civil(data_intimacao: date, dias: int) -> dict[str, Any]:
    """Calcula prazo civil (CPC arts. 219, 224, 220). Dias uteis, suspende no recesso."""
    anos = {data_intimacao.year, data_intimacao.year + 1}
    feriados: set[date] = set()
    for ano in anos:
        feriados |= _feriados_nacionais(ano)

    # Inicio: primeiro dia util apos intimacao
    inicio = data_intimacao + timedelta(days=1)
    inicio = _proximo_dia_util(inicio, feriados)
    # Pula recesso no inicio
    while _em_recesso_forense(inicio) or not _eh_dia_util(inicio, feriados):
        inicio += timedelta(days=1)
        if not _em_recesso_forense(inicio):
            inicio = _proximo_dia_util(inicio, feriados)

    contados = 0
    atual = inicio
    while contados < dias:
        if _eh_dia_util(atual, feriados) and not _em_recesso_forense(atual):
            contados += 1
            if contados == dias:
                break
        atual += timedelta(days=1)
        # Garantir feriados do ano corrente
        if atual.year not in anos:
            anos.add(atual.year)
            feriados |= _feriados_nacionais(atual.year)

    return {
        "tipo": "civil",
        "base_legal": "CPC arts. 219, 224, 220",
        "data_intimacao": data_intimacao.isoformat(),
        "dias_uteis": dias,
        "inicio_contagem": inicio.isoformat(),
        "vencimento": atual.isoformat(),
        "dia_semana_vencimento": _dia_semana_ptbr(atual),
        "feriados_no_periodo": _feriados_no_periodo(data_intimacao, atual, feriados),
        "aviso": "Apenas feriados nacionais. Verifique feriados estaduais/municipais da comarca.",
    }


def _prazo_criminal(data_intimacao: date, dias: int) -> dict[str, Any]:
    """Calcula prazo criminal (CPP art. 798). Dias corridos, NAO suspende no recesso."""
    anos = {data_intimacao.year, data_intimacao.year + 1}
    feriados: set[date] = set()
    for ano in anos:
        feriados |= _feriados_nacionais(ano)

    # Exclui dia do comeco, inclui dia do vencimento
    vencimento = data_intimacao + timedelta(days=dias)

    # Garantir feriados do ano do vencimento
    if vencimento.year not in anos:
        anos.add(vencimento.year)
        feriados |= _feriados_nacionais(vencimento.year)

    # Prorroga se vencer em dia nao util
    vencimento_original = vencimento
    vencimento = _proximo_dia_util(vencimento, feriados)

    resultado: dict[str, Any] = {
        "tipo": "criminal",
        "base_legal": "CPP art. 798",
        "data_intimacao": data_intimacao.isoformat(),
        "dias_corridos": dias,
        "vencimento": vencimento.isoformat(),
        "dia_semana_vencimento": _dia_semana_ptbr(vencimento),
        "feriados_no_periodo": _feriados_no_periodo(data_intimacao, vencimento, feriados),
        "aviso": "Apenas feriados nacionais. Verifique feriados estaduais/municipais da comarca.",
    }
    if vencimento != vencimento_original:
        resultado["prorrogado_de"] = vencimento_original.isoformat()

    return resultado


# --- Avaliador seguro de expressoes ---

_OPS_BINARIOS: dict[type, Any] = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.FloorDiv: operator.floordiv,
    ast.Mod: operator.mod,
    ast.Pow: operator.pow,
}

_OPS_UNARIOS: dict[type, Any] = {
    ast.UAdd: operator.pos,
    ast.USub: operator.neg,
}

_FUNCOES_PERMITIDAS: dict[str, Any] = {
    "abs": abs,
    "round": round,
    "sqrt": math.sqrt,
    "ceil": math.ceil,
    "floor": math.floor,
}

_MAX_EXPOENTE = 1000


def _avaliar_expressao(expr: str) -> float | int:
    """Avalia expressao matematica de forma segura via AST."""
    tree = ast.parse(expr, mode="eval")

    def _eval(node: ast.expr) -> float | int:
        if isinstance(node, ast.Expression):
            return _eval(node.body)
        if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
            return node.value
        if isinstance(node, ast.BinOp):
            op_func = _OPS_BINARIOS.get(type(node.op))
            if op_func is None:
                raise ValueError(f"Operador nao permitido: {type(node.op).__name__}")
            left = _eval(node.left)
            right = _eval(node.right)
            if (
                isinstance(node.op, ast.Pow)
                and isinstance(right, (int, float))
                and abs(right) > _MAX_EXPOENTE
            ):
                raise ValueError(f"Expoente {right} excede limite de {_MAX_EXPOENTE}")
            return op_func(left, right)
        if isinstance(node, ast.UnaryOp):
            op_func = _OPS_UNARIOS.get(type(node.op))
            if op_func is None:
                raise ValueError(f"Operador unario nao permitido: {type(node.op).__name__}")
            return op_func(_eval(node.operand))
        if isinstance(node, ast.Call):
            if not isinstance(node.func, ast.Name):
                raise ValueError("Chamada de funcao invalida")
            func = _FUNCOES_PERMITIDAS.get(node.func.id)
            if func is None:
                raise ValueError(
                    f"Funcao nao permitida: {node.func.id}. "
                    f"Permitidas: {', '.join(_FUNCOES_PERMITIDAS)}"
                )
            args = [_eval(arg) for arg in node.args]
            return func(*args)
        raise ValueError(f"Elemento nao permitido na expressao: {type(node).__name__}")

    return _eval(tree.body)


# --- Registro das tools ---


def register_utilidades_tools(mcp: FastMCP) -> None:
    """Registra tools utilitarias no servidor MCP."""

    @mcp.tool(
        annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
    )
    async def calculadora(
        modo: Literal["calcular", "prazo_civil", "prazo_criminal"],
        expressao: str | None = None,
        data_intimacao: str | None = None,
        dias: int | None = None,
    ) -> dict[str, Any]:
        """Calculadora juridica com 3 modos.

        modo="calcular": avalia expressao matematica (ex: "2 + 3 * 4", "sqrt(144)").
        Operadores: + - * / // % **. Funcoes: abs, round, sqrt, ceil, floor.

        modo="prazo_civil": prazo processual civil (CPC arts. 219, 224, 220).
        Contagem em dias uteis, exclui feriados nacionais, suspende no recesso (20/dez-20/jan).

        modo="prazo_criminal": prazo processual criminal (CPP art. 798).
        Dias corridos, prorroga se vencer em dia nao util. NAO suspende no recesso.

        Limitacoes: apenas feriados nacionais (sem estaduais/municipais), apenas dias (sem meses/anos).

        IMPORTANTE: a resposta inclui 'feriados_no_periodo' e 'aviso'. Verifique se ha
        feriados estaduais/municipais da comarca que possam afetar o prazo e informe ao usuario.
        """
        if modo == "calcular":
            if not expressao:
                return erro_instrutivo(
                    "Parametro 'expressao' obrigatorio para modo 'calcular'",
                    "Informe uma expressao matematica, ex: '2 + 3 * 4'",
                )
            try:
                resultado = _avaliar_expressao(expressao)
                return {
                    "expressao": expressao,
                    "resultado": resultado,
                }
            except (ValueError, SyntaxError, TypeError, ZeroDivisionError) as e:
                return erro_instrutivo(
                    f"Erro ao avaliar expressao: {e}",
                    "Verifique a sintaxe. Operadores: + - * / // % **. "
                    "Funcoes: abs, round, sqrt, ceil, floor.",
                )

        # Modos de prazo
        if not data_intimacao or dias is None:
            return erro_instrutivo(
                "Parametros 'data_intimacao' (AAAA-MM-DD) e 'dias' obrigatorios para calculo de prazo",
                "Ex: data_intimacao='2026-03-06', dias=15",
            )
        try:
            dt = date.fromisoformat(data_intimacao)
        except ValueError:
            return erro_instrutivo(
                f"Data invalida: '{data_intimacao}'",
                "Use formato AAAA-MM-DD, ex: '2026-03-06'",
            )
        if dias <= 0:
            return erro_instrutivo(
                "Prazo deve ser maior que zero",
                "Informe quantidade de dias positiva",
            )

        if modo == "prazo_civil":
            return _prazo_civil(dt, dias)
        else:
            return _prazo_criminal(dt, dias)

    @mcp.tool(
        annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
    )
    async def data_hora_atual(
        timezone: str = "America/Sao_Paulo",
    ) -> dict[str, Any]:
        """Retorna data e hora atuais no timezone especificado (padrao: America/Sao_Paulo).

        Util para referencia temporal em calculos de prazo e verificacao de prazos.
        """
        try:
            tz = ZoneInfo(timezone)
        except (ZoneInfoNotFoundError, KeyError):
            return erro_instrutivo(
                f"Timezone invalido: '{timezone}'",
                "Use formato IANA, ex: 'America/Sao_Paulo', 'America/Fortaleza', 'UTC'",
            )

        agora = datetime.now(tz)
        return {
            "data_iso": agora.date().isoformat(),
            "hora": agora.strftime("%H:%M:%S"),
            "dia_semana": _dia_semana_ptbr(agora.date()),
            "timestamp_utc": agora.astimezone(ZoneInfo("UTC")).isoformat(),
            "timezone": timezone,
        }
