"""
validate_ir.py — Validador de IR do Neuro Lang v0.1

Coerente com:
- neurotron/lang/ir.py  (ModuleIR, FunctionIR, Instruction, Op)

Objetivo v0.1 (fib-world):
- validar estrutura do IR
- validar opcodes e operandos
- validar CALL target + nargs
- stack safety mínima (linear)
- garantir que a função termina em RET

NOTA v0.1:
- Labels ainda são simbólicos (Op.LABEL com nome string).
- JUMP/JUMP_IF_FALSE ainda referenciam label_name (string), NÃO IP.
- O validador confirma que labels referenciadas existem na função.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from neurotron.lang.ir import ModuleIR, FunctionIR, Instruction, Op


# ---------------------------------------------------------------------
# Estrutura interna de erro
# ---------------------------------------------------------------------

@dataclass
class IRError:
    code: str
    msg: str
    func: str
    instr_index: Optional[int] = None

    def __str__(self) -> str:
        if self.instr_index is not None:
            return f"[{self.code}] {self.msg} in {self.func}: instr {self.instr_index}"
        return f"[{self.code}] {self.msg} in {self.func}"


# ---------------------------------------------------------------------
# API pública
# ---------------------------------------------------------------------

def validate_ir(module: ModuleIR) -> Tuple[str, List[IRError]]:
    """
    Valida o IR completo.
    Retorna ("IR_VALID", []) ou ("IR_INVALID", [erros...]).
    """
    errors: List[IRError] = []

    funcs = getattr(module, "functions", None)
    if not isinstance(funcs, dict):
        return ("IR_INVALID", [IRError("IR-00", "ModuleIR.functions não é dict", "<module>")])

    # 1) metadados básicos
    for fname, fn in funcs.items():
        _validate_function_metadata(fname, fn, errors)

    # 2) por função: labels, instruções, stack, retornos
    for fname, fn in funcs.items():
        label_defs = _collect_labels(fname, fn, errors)
        _validate_instructions(fname, fn, funcs, label_defs, errors)
        _validate_stack_safety(fname, fn, funcs, errors)
        _validate_returns(fname, fn, errors)

    if errors:
        return ("IR_INVALID", errors)
    return ("IR_VALID", [])


# ---------------------------------------------------------------------
# 3.1 metadata
# ---------------------------------------------------------------------

def _validate_function_metadata(fname: str, fn: FunctionIR, errors: List[IRError]) -> None:
    if not isinstance(fname, str) or not fname:
        errors.append(IRError("IR-02", "Invalid function name key in module.functions", "<module>"))

    if not isinstance(fn, FunctionIR):
        errors.append(IRError("IR-03", f"Function value is not FunctionIR: {type(fn).__name__}", fname))
        return

    if not isinstance(fn.name, str) or not fn.name:
        errors.append(IRError("IR-04", "FunctionIR.name inválido", fname))

    if not isinstance(fn.params, list) or any(not isinstance(p, str) or not p for p in fn.params):
        errors.append(IRError("IR-05", "FunctionIR.params inválido (esperado lista de strings)", fname))

    if not isinstance(fn.instructions, list):
        errors.append(IRError("IR-06", "FunctionIR.instructions inválido (esperado lista)", fname))


# ---------------------------------------------------------------------
# Labels (v0.1 simbólico)
# ---------------------------------------------------------------------

def _collect_labels(fname: str, fn: FunctionIR, errors: List[IRError]) -> set[str]:
    """
    Coleta labels definidos via Op.LABEL.
    """
    labels: set[str] = set()

    for i, instr in enumerate(fn.instructions):
        if not isinstance(instr, Instruction):
            errors.append(IRError("IR-20", f"Instruction inválida: {type(instr).__name__}", fname, i))
            continue

        if instr.op == Op.LABEL:
            if len(instr.args) != 1 or not isinstance(instr.args[0], str) or not instr.args[0]:
                errors.append(IRError("IR-12", "LABEL expects one non-empty string operand", fname, i))
            else:
                labels.add(instr.args[0])

    return labels


# ---------------------------------------------------------------------
# Instruções — formato e operandos
# ---------------------------------------------------------------------

def _validate_instructions(
    fname: str,
    fn: FunctionIR,
    func_table: Dict[str, FunctionIR],
    label_defs: set[str],
    errors: List[IRError],
) -> None:
    for i, instr in enumerate(fn.instructions):
        if not isinstance(instr, Instruction):
            errors.append(IRError("IR-20", f"Instruction inválida: {type(instr).__name__}", fname, i))
            continue

        op = instr.op
        args = instr.args

        if not isinstance(op, Op):
            errors.append(IRError("IR-20", f"Unknown opcode (not Op): {op!r}", fname, i))
            continue

        # -----------------------------------------------------------------
        # Op-specific operand rules (minimal fib-world)
        # -----------------------------------------------------------------

        if op == Op.PUSH_CONST:
            if len(args) != 1 or not isinstance(args[0], int):
                errors.append(IRError("IR-21", "PUSH_CONST expects 1 integer operand", fname, i))

        elif op == Op.LOAD_VAR:
            if len(args) != 1 or not isinstance(args[0], str) or not args[0]:
                errors.append(IRError("IR-22", "LOAD_VAR expects 1 non-empty string operand", fname, i))
            else:
                varname = args[0]
                if varname not in fn.params:
                    errors.append(
                        IRError("IR-22", f"LOAD_VAR refers to unknown param '{varname}'", fname, i)
                    )

        elif op == Op.STORE_VAR:
            # não usado no fib, mas previsto: STORE_VAR name
            if len(args) != 1 or not isinstance(args[0], str) or not args[0]:
                errors.append(IRError("IR-25", "STORE_VAR expects 1 non-empty string operand", fname, i))

        elif op in (Op.ADD, Op.SUB, Op.LT, Op.RET):
            if len(args) != 0:
                errors.append(IRError("IR-26", f"{op.name} expects 0 operands", fname, i))

        elif op == Op.CALL:
            # CALL func_name nargs
            if len(args) != 2 or not isinstance(args[0], str) or not isinstance(args[1], int):
                errors.append(IRError("IR-23", "CALL expects operands: (func_name:str, nargs:int)", fname, i))
            else:
                func_name, nargs = args[0], args[1]
                if func_name not in func_table:
                    errors.append(IRError("IR-23", f"CALL target not found: {func_name}", fname, i))
                else:
                    expected = len(func_table[func_name].params)
                    if nargs != expected:
                        errors.append(
                            IRError("IR-23", f"CALL {func_name} expects {expected} args, got {nargs}", fname, i)
                        )
                if nargs < 0:
                    errors.append(IRError("IR-23", "CALL nargs cannot be negative", fname, i))

        elif op in (Op.JUMP, Op.JUMP_IF_FALSE):
            # v0.1: operand é label_name (string)
            if len(args) != 1 or not isinstance(args[0], str) or not args[0]:
                errors.append(IRError("IR-24", f"{op.name} expects 1 label_name (string)", fname, i))
            else:
                label = args[0]
                if label not in label_defs:
                    errors.append(IRError("IR-10", f"Jump to undefined label: {label}", fname, i))

        elif op == Op.LABEL:
            # validado em _collect_labels
            pass

        else:
            errors.append(IRError("IR-20", f"Opcode not supported by validator v0.1: {op.name}", fname, i))


# ---------------------------------------------------------------------
# Stack safety mínima (linear)
# ---------------------------------------------------------------------

def _validate_stack_safety(
    fname: str,
    fn: FunctionIR,
    func_table: Dict[str, FunctionIR],
    errors: List[IRError],
) -> None:
    """
    Simulação linear (não-CFG) para apanhar underflow óbvios.

    Regras mínimas:
    - PUSH_CONST, LOAD_VAR: +1
    - STORE_VAR: -1
    - ADD/SUB/LT: pop2 push1  => -1 net (mas requer >=2)
    - JUMP_IF_FALSE: pop1      => -1 (requer >=1)
    - CALL name nargs: pop(nargs) push1  => -(nargs) + 1 (requer >= nargs)
    - RET: pop1 (requer >=1)
    - LABEL/JUMP: 0
    """
    depth = 0

    for i, instr in enumerate(fn.instructions):
        if not isinstance(instr, Instruction) or not isinstance(instr.op, Op):
            continue

        op = instr.op

        if op in (Op.PUSH_CONST, Op.LOAD_VAR):
            depth += 1

        elif op == Op.STORE_VAR:
            if depth < 1:
                errors.append(IRError("IR-32", "Stack underflow on STORE_VAR", fname, i))
                depth = 0
            else:
                depth -= 1

        elif op in (Op.ADD, Op.SUB, Op.LT):
            if depth < 2:
                errors.append(IRError("IR-30", f"{op.name} requires stack >= 2", fname, i))
                depth = max(depth, 0)
            else:
                # pop2 push1 => depth -= 1
                depth -= 1

        elif op == Op.JUMP_IF_FALSE:
            if depth < 1:
                errors.append(IRError("IR-34", "JUMP_IF_FALSE requires stack >= 1", fname, i))
                depth = 0
            else:
                depth -= 1

        elif op == Op.CALL:
            # CALL func nargs
            if len(instr.args) == 2 and isinstance(instr.args[1], int):
                nargs = instr.args[1]
            else:
                # já vai haver erro no validador de operands; aqui só evitamos crash
                nargs = 0

            if nargs < 0:
                errors.append(IRError("IR-33", "CALL nargs negative", fname, i))
                nargs = 0

            if depth < nargs:
                errors.append(
                    IRError("IR-33", f"CALL requires stack >= {nargs} (args), current={depth}", fname, i)
                )
                depth = 0
            else:
                # pop nargs, push return value
                depth = depth - nargs + 1

        elif op == Op.RET:
            if depth < 1:
                errors.append(IRError("IR-31", "RET requires stack >= 1", fname, i))
                depth = 0
            else:
                depth -= 1
            # após RET, numa simulação linear não sabemos o fluxo, mas seguimos.

        elif op in (Op.JUMP, Op.LABEL):
            pass

        # nunca negativo
        if depth < 0:
            errors.append(IRError("IR-32", "Stack underflow (negative depth)", fname, i))
            depth = 0


# ---------------------------------------------------------------------
# Cada função deve terminar em RET
# ---------------------------------------------------------------------

def _validate_returns(fname: str, fn: FunctionIR, errors: List[IRError]) -> None:
    instrs = fn.instructions
    if not instrs:
        errors.append(IRError("IR-40", "Empty function missing RET", fname))
        return

    last = instrs[-1]
    if not isinstance(last, Instruction) or last.op != Op.RET:
        errors.append(IRError("IR-40", "Missing RET at end of function", fname))
