"""
event_validator.py — Validación estricta de eventos antes de escribir rxdata.

Actúa como firewall entre los templates/LLM y el writer.rb.
Si la validación falla, lanza EventValidationError con el problema exacto.
Nunca se escribe en disco si este módulo lanza excepción.

Principio: es mejor rechazar un evento válido que corromper el rxdata.
"""

from __future__ import annotations
from typing import Any


class EventValidationError(Exception):
    """Describe exactamente qué está mal y dónde."""
    pass


# =============================================================================
# CONSTANTES — se completan al final del módulo tras definir los validadores
# =============================================================================

_CMD_SPECS: dict[int, tuple[str, Any]] = {}

_VALID_SELF_SWITCHES = {"A", "B", "C", "D"}
_MAX_TEXT_LINE_LEN   = 50   # PE v21.1 muestra ~50 chars por línea antes de cortar
_MAX_PAGES           = 20   # límite razonable de páginas por evento
_MAX_COMMANDS        = 500  # límite razonable de comandos por página


# =============================================================================
# VALIDADORES DE COMANDOS ESPECÍFICOS
# =============================================================================

def _validate_cmd_101(params: list, ctx: str):
    """Show Text header: ["face_name", face_index, position, window_type]"""
    if not isinstance(params, list):
        raise EventValidationError(f"{ctx}: CMD 101 parameters debe ser lista, got {type(params)}")
    # Rellenar con defaults si faltan campos — RMXP acepta lista parcial
    # pero debemos asegurar que los presentes son del tipo correcto
    if len(params) > 0 and not isinstance(params[0], str):
        raise EventValidationError(f"{ctx}: CMD 101 params[0] (face_name) debe ser string")
    if len(params) > 1 and not isinstance(params[1], int):
        raise EventValidationError(f"{ctx}: CMD 101 params[1] (face_index) debe ser int")
    if len(params) > 2 and params[2] not in (0, 1, 2):
        raise EventValidationError(f"{ctx}: CMD 101 params[2] (position) debe ser 0,1 o 2")


def _validate_cmd_401(params: list, ctx: str):
    """Show Text line: ["texto"]"""
    if not isinstance(params, list) or len(params) == 0:
        raise EventValidationError(f"{ctx}: CMD 401 parameters debe ser lista con al menos 1 elemento")
    if not isinstance(params[0], str):
        raise EventValidationError(f"{ctx}: CMD 401 params[0] (texto) debe ser string")


def _validate_cmd_121(params: list, ctx: str):
    """Control Switches: [start_id, end_id, operation(0=ON,1=OFF,2=toggle)]"""
    if not isinstance(params, list) or len(params) < 3:
        raise EventValidationError(f"{ctx}: CMD 121 requiere [start_id, end_id, operation]")
    if not isinstance(params[0], int) or params[0] < 1:
        raise EventValidationError(f"{ctx}: CMD 121 params[0] (switch_id) debe ser entero >= 1")
    if params[2] not in (0, 1, 2):
        raise EventValidationError(f"{ctx}: CMD 121 params[2] (operation) debe ser 0, 1 o 2")


def _validate_cmd_123(params: list, ctx: str):
    """Control Self Switch: ["A"/"B"/"C"/"D", 0=ON/1=OFF]"""
    if not isinstance(params, list) or len(params) < 2:
        raise EventValidationError(f"{ctx}: CMD 123 requiere [letra, value]")
    if params[0] not in _VALID_SELF_SWITCHES:
        raise EventValidationError(
            f"{ctx}: CMD 123 params[0] debe ser A, B, C o D — got '{params[0]}'"
        )
    if params[1] not in (0, 1):
        raise EventValidationError(f"{ctx}: CMD 123 params[1] debe ser 0 (ON) o 1 (OFF)")


def _validate_cmd_201(params: list, ctx: str):
    """Transfer Player: [mode, map_id, x, y, direction, fade]"""
    if not isinstance(params, list) or len(params) < 4:
        raise EventValidationError(f"{ctx}: CMD 201 requiere [mode, map_id, x, y, direction, fade]")
    if params[0] not in (0, 1, 2):
        raise EventValidationError(f"{ctx}: CMD 201 params[0] (mode) debe ser 0, 1 o 2")
    if not isinstance(params[1], int) or params[1] < 1:
        raise EventValidationError(f"{ctx}: CMD 201 params[1] (map_id) debe ser entero >= 1")
    if not isinstance(params[2], int) or params[2] < 0:
        raise EventValidationError(f"{ctx}: CMD 201 params[2] (x) debe ser entero >= 0")
    if not isinstance(params[3], int) or params[3] < 0:
        raise EventValidationError(f"{ctx}: CMD 201 params[3] (y) debe ser entero >= 0")
    if len(params) > 4 and params[4] not in (0, 2, 4, 6, 8):
        raise EventValidationError(
            f"{ctx}: CMD 201 params[4] (direction) debe ser 0,2,4,6 u 8"
        )


def _validate_cmd_355(params: list, ctx: str):
    """Script inline: ["codigo_ruby"]"""
    if not isinstance(params, list) or len(params) == 0:
        raise EventValidationError(f"{ctx}: CMD 355 requiere [\"codigo_ruby\"]")
    if not isinstance(params[0], str):
        raise EventValidationError(f"{ctx}: CMD 355 params[0] debe ser string con código Ruby")
    if len(params[0].strip()) == 0:
        raise EventValidationError(f"{ctx}: CMD 355 params[0] no puede ser string vacío")


# Actualizar _CMD_SPECS con los validadores ahora definidos
_CMD_SPECS[101] = ("Show Text",           _validate_cmd_101)
_CMD_SPECS[401] = ("Show Text Line",      _validate_cmd_401)
_CMD_SPECS[121] = ("Control Switches",    _validate_cmd_121)
_CMD_SPECS[123] = ("Control Self Switch", _validate_cmd_123)
_CMD_SPECS[201] = ("Transfer Player",     _validate_cmd_201)
_CMD_SPECS[355] = ("Script",              _validate_cmd_355)


# =============================================================================
# VALIDADORES DE ESTRUCTURA
# =============================================================================

def validate_command(cmd: Any, index: int, page_ctx: str):
    """Valida un único EventCommand."""
    ctx = f"{page_ctx} cmd[{index}]"

    if not isinstance(cmd, dict):
        raise EventValidationError(f"{ctx}: comando debe ser dict, got {type(cmd)}")

    # code
    if "code" not in cmd:
        raise EventValidationError(f"{ctx}: falta campo 'code'")
    code = cmd["code"]
    if not isinstance(code, int):
        raise EventValidationError(f"{ctx}: 'code' debe ser int, got {type(code)}")

    # indent
    indent = cmd.get("indent", 0)
    if not isinstance(indent, int) or indent < 0:
        raise EventValidationError(f"{ctx}: 'indent' debe ser entero >= 0")

    # parameters
    params = cmd.get("parameters", [])
    if not isinstance(params, list):
        raise EventValidationError(
            f"{ctx}: 'parameters' debe ser lista, got {type(params)}. "
            f"(code={code})"
        )

    # Validar parámetros específicos si tenemos spec
    if code in _CMD_SPECS:
        _, validator = _CMD_SPECS[code]
        if validator is not None:
            validator(params, ctx)


def validate_condition(cond: Any, ctx: str):
    """Valida RPG::Event::Page::Condition."""
    if not isinstance(cond, dict):
        raise EventValidationError(f"{ctx} condition: debe ser dict")

    bool_fields = [
        "switch1_valid", "switch2_valid", "variable_valid", "self_switch_valid"
    ]
    for f in bool_fields:
        if f in cond and not isinstance(cond[f], bool):
            raise EventValidationError(
                f"{ctx} condition.{f}: debe ser bool, got {type(cond[f])}"
            )

    if "self_switch_ch" in cond:
        if cond["self_switch_ch"] not in _VALID_SELF_SWITCHES:
            raise EventValidationError(
                f"{ctx} condition.self_switch_ch: debe ser A,B,C o D — "
                f"got '{cond['self_switch_ch']}'"
            )


def validate_graphic(graphic: Any, ctx: str):
    """Valida RPG::Event::Page::Graphic."""
    if not isinstance(graphic, dict):
        raise EventValidationError(f"{ctx} graphic: debe ser dict")

    if "character_name" in graphic and not isinstance(graphic["character_name"], str):
        raise EventValidationError(f"{ctx} graphic.character_name: debe ser string")

    if "direction" in graphic and graphic["direction"] not in (0, 2, 4, 6, 8):
        raise EventValidationError(
            f"{ctx} graphic.direction: debe ser 0,2,4,6 u 8 — "
            f"got {graphic['direction']}"
        )

    if "opacity" in graphic:
        op = graphic["opacity"]
        if not isinstance(op, int) or not (0 <= op <= 255):
            raise EventValidationError(
                f"{ctx} graphic.opacity: debe ser int 0-255 — got {op}"
            )


def validate_move_route(mr: Any, ctx: str):
    """Valida RPG::MoveRoute."""
    if not isinstance(mr, dict):
        raise EventValidationError(f"{ctx} move_route: debe ser dict")

    if "list" not in mr:
        raise EventValidationError(f"{ctx} move_route: falta campo 'list'")

    if not isinstance(mr["list"], list):
        raise EventValidationError(f"{ctx} move_route.list: debe ser lista")

    # Cada MoveCommand debe tener code y parameters
    for i, mc in enumerate(mr["list"]):
        if not isinstance(mc, dict):
            raise EventValidationError(
                f"{ctx} move_route.list[{i}]: debe ser dict"
            )
        if "code" not in mc:
            raise EventValidationError(
                f"{ctx} move_route.list[{i}]: falta 'code'"
            )
        if not isinstance(mc.get("parameters", []), list):
            raise EventValidationError(
                f"{ctx} move_route.list[{i}].parameters: debe ser lista"
            )


def validate_page(page: Any, page_idx: int, event_ctx: str):
    """Valida una RPG::Event::Page completa."""
    ctx = f"{event_ctx} page[{page_idx}]"

    if not isinstance(page, dict):
        raise EventValidationError(f"{ctx}: página debe ser dict")

    # trigger
    trigger = page.get("trigger", 0)
    if trigger not in (0, 1, 2, 3, 4):
        raise EventValidationError(f"{ctx}: trigger debe ser 0-4, got {trigger}")

    # move_type
    move_type = page.get("move_type", 0)
    if move_type not in (0, 1, 2, 3):
        raise EventValidationError(f"{ctx}: move_type debe ser 0-3, got {move_type}")

    # move_speed
    move_speed = page.get("move_speed", 3)
    if not isinstance(move_speed, int) or not (1 <= move_speed <= 6):
        raise EventValidationError(f"{ctx}: move_speed debe ser 1-6, got {move_speed}")

    # condition
    if "condition" in page:
        validate_condition(page["condition"], ctx)

    # graphic
    if "graphic" in page:
        validate_graphic(page["graphic"], ctx)

    # move_route
    if "move_route" in page:
        validate_move_route(page["move_route"], ctx)

    # list (comandos)
    cmd_list = page.get("list")
    if cmd_list is None:
        raise EventValidationError(f"{ctx}: falta campo 'list' (lista de comandos)")
    if not isinstance(cmd_list, list):
        raise EventValidationError(f"{ctx}: 'list' debe ser lista")
    if len(cmd_list) == 0:
        raise EventValidationError(f"{ctx}: 'list' no puede estar vacía (necesita al menos CMD 0)")
    if len(cmd_list) > _MAX_COMMANDS:
        raise EventValidationError(
            f"{ctx}: demasiados comandos ({len(cmd_list)} > {_MAX_COMMANDS})"
        )

    # Validar cada comando
    for i, cmd in enumerate(cmd_list):
        validate_command(cmd, i, ctx)

    # El último comando DEBE ser code=0 (End)
    last = cmd_list[-1]
    if not isinstance(last, dict) or last.get("code") != 0:
        raise EventValidationError(
            f"{ctx}: el último comando debe ser code=0 (End), "
            f"got code={last.get('code') if isinstance(last, dict) else last}"
        )

    # Verificar que CMD 401 siempre va después de CMD 101
    prev_code = None
    for i, cmd in enumerate(cmd_list):
        if not isinstance(cmd, dict):
            continue
        code = cmd.get("code")
        if code == 401 and prev_code not in (101, 401):
            raise EventValidationError(
                f"{ctx} cmd[{i}]: CMD 401 (texto) sin CMD 101 previo — "
                f"el código anterior es {prev_code}"
            )
        prev_code = code


def validate_event(event: Any, map_id: int = 0) -> None:
    """
    Valida un evento completo antes de enviarlo al bridge.
    Lanza EventValidationError con descripción precisa del problema.
    """
    ctx = f"[map={map_id}]"

    if not isinstance(event, dict):
        raise EventValidationError(f"{ctx}: evento debe ser dict, got {type(event)}")

    # Campos obligatorios del evento
    for field in ("name", "x", "y", "pages"):
        if field not in event:
            raise EventValidationError(f"{ctx}: falta campo obligatorio '{field}'")

    # name
    if not isinstance(event["name"], str):
        raise EventValidationError(f"{ctx}: 'name' debe ser string")
    if len(event["name"]) == 0:
        raise EventValidationError(f"{ctx}: 'name' no puede estar vacío")

    # coordenadas
    x, y = event["x"], event["y"]
    if not isinstance(x, int) or x < 0:
        raise EventValidationError(f"{ctx}: 'x' debe ser entero >= 0, got {x}")
    if not isinstance(y, int) or y < 0:
        raise EventValidationError(f"{ctx}: 'y' debe ser entero >= 0, got {y}")

    # pages
    pages = event["pages"]
    if not isinstance(pages, list):
        raise EventValidationError(f"{ctx}: 'pages' debe ser lista")
    if len(pages) == 0:
        raise EventValidationError(f"{ctx}: 'pages' no puede estar vacía")
    if len(pages) > _MAX_PAGES:
        raise EventValidationError(
            f"{ctx}: demasiadas páginas ({len(pages)} > {_MAX_PAGES})"
        )

    for i, page in enumerate(pages):
        validate_page(page, i, ctx)


def validate_event_safe(event: Any, map_id: int = 0) -> tuple[bool, str]:
    """
    Versión no-lanzadora. Devuelve (ok, mensaje).
    Útil para reportar errores al usuario sin interrumpir el flujo.
    """
    try:
        validate_event(event, map_id)
        return True, "OK"
    except EventValidationError as e:
        return False, str(e)


# =============================================================================
# REGISTRAR SPECS — aquí sí están definidos todos los validadores
# =============================================================================

_CMD_SPECS.update({
    0:   ("End",                 None),
    101: ("Show Text",           _validate_cmd_101),
    401: ("Show Text Line",      _validate_cmd_401),
    102: ("Show Choices",        None),
    111: ("Conditional Branch",  None),
    121: ("Control Switches",    _validate_cmd_121),
    122: ("Control Variables",   None),
    123: ("Control Self Switch", _validate_cmd_123),
    201: ("Transfer Player",     _validate_cmd_201),
    355: ("Script",              _validate_cmd_355),
    411: ("Else Branch",         None),
    412: ("End Branch",          None),
})


# =============================================================================
# VALIDADOR DE MAPA COMPLETO
# Para verificar integridad tras leer un rxdata existente
# =============================================================================

def validate_map_events(map_data: dict, map_id: int = 0) -> list[str]:
    """
    Valida todos los eventos de un mapa leído del rxdata.
    Devuelve lista de errores encontrados (vacía si todo OK).
    """
    errors = []
    events = map_data.get("events") or {}
    for eid, ev in events.items():
        if not isinstance(ev, dict):
            errors.append(f"Evento {eid}: no es dict")
            continue
        ok, msg = validate_event_safe(ev, map_id)
        if not ok:
            errors.append(f"Evento {eid} ({ev.get('name', '?')}): {msg}")
    return errors
