# apps/aroflo_connector_app/agent/executor.py
from __future__ import annotations

import json
import logging
from typing import Any, Dict, Tuple

from ..client import AroFloClient
from ..config import AroFloConfigError
from ..zones.registry import build_registry
from .tool_schema import TOOL_NAME_MAP

log = logging.getLogger("aroflo_agent.executor")


def _coerce_args(tool_args: Any) -> Dict[str, Any]:
    if tool_args is None:
        return {}
    if isinstance(tool_args, dict):
        return tool_args
    if isinstance(tool_args, str):
        s = tool_args.strip()
        if not s:
            return {}
        try:
            obj = json.loads(s)
            return obj if isinstance(obj, dict) else {"_raw": obj}
        except Exception:
            return {"_raw": tool_args}
    return {"_raw": tool_args}


def _normalize_where_clause(where: Any) -> Any:
    """
    Normaliza cláusulas where estilo AroFlo para evitar variaciones:
    - convierte " AND " a separador ';' cuando es un where con pipes
    - asegura prefijo and| / or|
    """
    if not isinstance(where, str):
        return where

    w = where.strip()
    if not w:
        return w

    # Solo normalizamos si parece where de AroFlo (tiene pipes)
    if "|" not in w:
        return w

    w = w.replace(" AND ", " and ")
    w = w.replace(" and|", ";and|").replace(" AND|", ";and|")
    w = w.replace("|and|", ";and|")

    # Unifica separadores " and " SOLO en formato con pipes
    w = w.replace(" and ", ";")

    while ";;" in w:
        w = w.replace(";;", ";")

    w = w.strip(";")

    parts = [p.strip() for p in w.split(";") if p.strip()]
    fixed = []
    for p in parts:
        pl = p.lower()
        if pl.startswith("and|") or pl.startswith("or|"):
            fixed.append(p)
        else:
            fixed.append("and|" + p)

    return ";".join(fixed)


def _parse_tool_name_legacy(tool_name: str) -> Tuple[str, str]:
    """
    Formatos legacy soportados:
      - aroflo__<zone>__<op>
      - <zone>__<op>
      - aroflo.<zone>.<op>
      - <zone>.<op>
      - <zone>_<op>   (ultra legacy)
    """
    name = (tool_name or "").strip()
    if not name:
        raise ValueError("tool_name vacío")

    # 1) "__"
    if "__" in name:
        parts = [p for p in name.split("__") if p]
        if parts and parts[0].lower() == "aroflo":
            parts = parts[1:]
        if len(parts) < 2:
            raise ValueError(f"tool_name inválido '{tool_name}' (esperado '__zone__op').")
        zone_code = parts[0]
        op_code = "__".join(parts[1:])  # por si un día el op tuviera "__"
        return zone_code, op_code

    # 2) "."
    if "." in name:
        parts = [p for p in name.split(".") if p]
        if parts and parts[0].lower() == "aroflo":
            parts = parts[1:]
        if len(parts) < 2:
            raise ValueError(f"tool_name inválido '{tool_name}' (esperado 'zone.op').")
        zone_code = parts[0]
        op_code = ".".join(parts[1:])
        return zone_code, op_code

    # 3) "_" (fallback)
    if "_" in name:
        zone_code, op_code = name.split("_", 1)
        return zone_code, op_code

    raise ValueError(f"tool_name inválido '{tool_name}'. No pude inferir zona/operación.")


def _resolve_tool_to_operation(tool_name: str) -> Tuple[str, str]:
    """
    Devuelve (zone_code, op_code) real.

    Prioridad:
      1) TOOL_NAME_MAP (para nombres truncados+hash; fuente de verdad)
      2) Parser legacy
    """
    mapped = TOOL_NAME_MAP.get(tool_name)
    if mapped:
        return mapped
    return _parse_tool_name_legacy(tool_name)


def execute_tool_call(tool_name: str, tool_args: Any) -> Dict[str, Any]:
    """
    Ejecuta una tool (read o write) directamente.

    Nota importante:
    - La confirmación NO se maneja aquí.
    - El control de confirmación se hace en agent_cli.py vía PROPOSED_TOOL + pending_store + CONFIRM.
    """
    args = _coerce_args(tool_args)

    try:
        client = AroFloClient()
    except AroFloConfigError as exc:
        return {
            "ok": False,
            "tool_name": tool_name,
            "error": {"type": "AroFloConfigError", "message": str(exc)},
        }
    except Exception as exc:
        return {
            "ok": False,
            "tool_name": tool_name,
            "error": {"type": exc.__class__.__name__, "message": str(exc)},
        }

    try:
        registry = build_registry(client)

        zone_code, op_code = _resolve_tool_to_operation(tool_name)

        if zone_code not in registry:
            known = ", ".join(sorted(registry.keys()))
            raise ValueError(f"Zona '{zone_code}' no existe. Zonas conocidas: {known}")

        zone = registry[zone_code]

        # Normalización defensiva del WHERE
        if isinstance(args, dict) and "where" in args:
            args["where"] = _normalize_where_clause(args["where"])

        # Metadata (solo informativa)
        op_meta = None
        try:
            op_meta = next((o for o in zone.operations if o.code == op_code), None)
        except Exception:
            op_meta = None

        requires_confirmation = bool(getattr(op_meta, "requires_confirmation", False))

        log.info(
            "Tool call -> tool=%s resolved=(%s,%s) args=%s confirm_required=%s",
            tool_name,
            zone_code,
            op_code,
            args,
            requires_confirmation,
        )

        result = zone.execute(op_code, params=args)

        return {
            "ok": True,
            "tool_name": tool_name,
            "zone": zone_code,
            "operation": op_code,
            "requires_confirmation": requires_confirmation,
            "data": result,
        }

    except Exception as exc:
        return {
            "ok": False,
            "tool_name": tool_name,
            "error": {"type": exc.__class__.__name__, "message": str(exc)},
            "debug": {
                "args": args,
                "resolved": TOOL_NAME_MAP.get(tool_name),
            },
        }
