#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
sipi_depurador.py -- Depurador CLI real para SiPi (item 3 del roadmap).

A diferencia de la bandera '--depurar' (que solo imprime cada linea antes
de ejecutarla, sin forma de pausar ni inspeccionar nada), esto es un
depurador interactivo de verdad: breakpoints por numero de linea, paso a
paso, inspeccion de variables (locales y globales) en el punto exacto
donde el programa esta detenido, y continuar hasta el proximo breakpoint.

Se apoya en dos puntos de extension que YA existian en el interprete
(usados hasta ahora solo por el depurador visual del editor de escritorio):
  - 'interprete.hook_linea': una funcion que el interprete llama antes de
    ejecutar cada linea, con (numero_de_linea, texto_de_la_linea).
  - 'DepuracionDetenida': la excepcion que corta la ejecucion por completo
    (usada por el comando 'salir' de este depurador).

Corre en el mismo proceso que el programa que se esta depurando (no un
subproceso aparte) porque necesita acceso directo a
'interprete.entorno.variables' y 'interprete.pila_scopes' para poder
mostrar el valor real de las variables en el momento exacto de la pausa --
algo que un depurador que solo mira los prints de un subproceso no podria
hacer nunca.
"""
import os
import sys

AQUI = os.path.dirname(os.path.abspath(__file__))
if AQUI not in sys.path:
    sys.path.insert(0, AQUI)

import sipi as motor_sipi  # noqa: E402


class _EstadoDepurador:
    def __init__(self, interprete, ruta_archivo):
        self.interprete = interprete
        self.ruta_archivo = ruta_archivo
        self.breakpoints = set()
        self.modo_paso = False  # True: se detiene en TODAS las lineas, no solo en breakpoints
        self.ultimo_comando = ""
        self.terminado = False
        self.terminado_por_usuario = False

    def _mostrar_variables(self):
        # Locales (si estamos dentro de una llamada a funcion) primero,
        # despues globales -- asi lo mas relevante para donde esta
        # parado el programa aparece arriba, no perdido entre las
        # globales.
        scopes_locales = getattr(self.interprete, "pila_scopes", [])
        if scopes_locales:
            print("  Variables locales (funcion actual):")
            for nombre, valor in scopes_locales[-1].items():
                print(f"    {nombre} = {valor!r}")
        variables_globales = self.interprete.entorno.variables
        if variables_globales:
            print("  Variables globales:")
            for nombre, valor in variables_globales.items():
                if nombre in ("PI", "E"):
                    continue  # constantes matematicas de siempre, no aportan nada verlas en cada pausa
                print(f"    {nombre} = {valor!r}")
        if not scopes_locales and not variables_globales:
            print("  (todavia no hay ninguna variable declarada)")

    def _mostrar_ayuda(self):
        print("""
  Comandos del depurador:
    paso, p              -- ejecuta esta linea y se detiene en la siguiente
    continuar, c          -- sigue corriendo hasta el proximo breakpoint (o hasta el final)
    b <numero_de_linea>   -- agrega un breakpoint en esa linea
    eliminar_b <numero>   -- quita un breakpoint
    breakpoints, bl        -- lista los breakpoints activos
    variables, v          -- muestra las variables locales y globales en este punto
    ver <nombre>          -- muestra el valor de una variable puntual
    linea                 -- vuelve a mostrar la linea actual
    ayuda, h, ?           -- esta ayuda
    salir, q              -- corta la ejecucion del programa completo
    (Enter sin nada)       -- repite el ultimo comando (comodo para tirar 'paso' varias veces)
""")

    def _procesar_comando(self, comando, num, linea):
        """Devuelve True si hay que seguir esperando comandos (quedarse
        pausado), o False si hay que reanudar la ejecucion del programa."""
        comando = comando.strip()
        if comando == "" and self.ultimo_comando:
            comando = self.ultimo_comando
        partes = comando.split(None, 1)
        palabra = partes[0] if partes else ""
        resto = partes[1].strip() if len(partes) > 1 else ""

        if palabra in ("paso", "p"):
            self.modo_paso = True
            self.ultimo_comando = "paso"
            return False

        if palabra in ("continuar", "c"):
            self.modo_paso = False
            self.ultimo_comando = "continuar"
            return False

        if palabra == "b":
            if not resto.isdigit():
                print("  Uso: b <numero_de_linea>")
                return True
            self.breakpoints.add(int(resto))
            print(f"  Breakpoint agregado en la linea {resto}.")
            return True

        if palabra == "eliminar_b":
            if not resto.isdigit() or int(resto) not in self.breakpoints:
                print("  Ese numero no es un breakpoint activo.")
                return True
            self.breakpoints.discard(int(resto))
            print(f"  Breakpoint de la linea {resto} eliminado.")
            return True

        if palabra in ("breakpoints", "bl"):
            if self.breakpoints:
                print("  Breakpoints activos: " + ", ".join(str(n) for n in sorted(self.breakpoints)))
            else:
                print("  No hay breakpoints activos.")
            return True

        if palabra in ("variables", "v"):
            self._mostrar_variables()
            return True

        if palabra == "ver":
            if not resto:
                print("  Uso: ver <nombre_de_variable>")
                return True
            valor, encontrada = self.interprete._buscar_variable(resto)
            if encontrada:
                print(f"  {resto} = {valor!r}")
            else:
                print(f"  No existe ninguna variable llamada '{resto}' en este punto.")
            return True

        if palabra == "linea":
            print(f"  {num} | {linea}")
            return True

        if palabra in ("ayuda", "h", "?"):
            self._mostrar_ayuda()
            return True

        if palabra in ("salir", "q"):
            print("  Cortando la ejecucion del programa.")
            self.terminado_por_usuario = True
            raise motor_sipi.DepuracionDetenida()

        print(f"  Comando desconocido: '{comando}'. Escribi 'ayuda' para ver los comandos disponibles.")
        return True

    def hook_linea(self, num, linea):
        if not (self.modo_paso or num in self.breakpoints):
            return
        motivo = "breakpoint" if num in self.breakpoints and not self.modo_paso else "paso a paso"
        print(f"\n[SiPi Depurador] Detenido en linea {num} ({motivo})")
        print(f"  {num} | {linea}")
        while True:
            try:
                comando = input("(sipi-depurar) ").strip()
            except EOFError:
                print("\n[SiPi Depurador] Entrada cerrada, dejo el programa correr hasta el final.")
                self.modo_paso = False
                self.breakpoints.clear()
                return
            try:
                seguir_pausado = self._procesar_comando(comando, num, linea)
            except motor_sipi.DepuracionDetenida:
                raise
            if not seguir_pausado:
                return


def ejecutar_con_depurador(ruta_archivo, breakpoints_iniciales=None):
    """Corre 'ruta_archivo' con el depurador CLI activo. 'breakpoints_iniciales'
    es una lista opcional de numeros de linea para arrancar ya con
    breakpoints puestos (comodo para no tener que tipearlos apenas arranca).
    """
    if not os.path.exists(ruta_archivo):
        print(f"[SiPi] Error: no se encontro el archivo '{ruta_archivo}'")
        sys.exit(1)

    interprete = motor_sipi.Interprete(ruta_archivo)
    estado = _EstadoDepurador(interprete, ruta_archivo)
    for numero in (breakpoints_iniciales or []):
        estado.breakpoints.add(numero)

    interprete.hook_linea = estado.hook_linea

    print(f"[SiPi Depurador] Depurando '{ruta_archivo}'.")
    if estado.breakpoints:
        print(f"[SiPi Depurador] Breakpoints iniciales: {', '.join(str(n) for n in sorted(estado.breakpoints))}")
    else:
        print("[SiPi Depurador] Sin breakpoints todavia -- el programa va a correr de punta a punta.")
        print("[SiPi Depurador] Agrega uno en cualquier pausa con 'b <numero_de_linea>', o tirá 'paso' para ir linea por linea desde el principio.")
    estado._mostrar_ayuda()

    try:
        interprete.ejecutar()
        # 'interprete.ejecutar()' atrapa 'DepuracionDetenida' internamente
        # (es el mismo mecanismo que usa el boton "Detener" del depurador
        # visual del editor: corta la ejecucion en silencio en vez de
        # dejar que la excepcion se propague como un error), asi que la
        # unica forma de saber si termino solo o porque el usuario tipeo
        # 'salir' es la bandera que 'hook_linea' dejo marcada.
        if estado.terminado_por_usuario:
            print("\n[SiPi Depurador] Ejecucion cortada por el usuario.")
        else:
            print("\n[SiPi Depurador] El programa termino normalmente.")
    except motor_sipi.SiPiError as e:
        print(f"\n[SiPi Depurador] El programa termino con un error:\n{e}")
        sys.exit(1)


def main():
    argumentos = sys.argv[1:]
    if not argumentos:
        print("Uso: sipi_depurador.py archivo.sipi [--breakpoint N ...]")
        sys.exit(1)
    ruta_archivo = argumentos[0]
    breakpoints_iniciales = []
    i = 1
    while i < len(argumentos):
        if argumentos[i] == "--breakpoint" and i + 1 < len(argumentos) and argumentos[i + 1].isdigit():
            breakpoints_iniciales.append(int(argumentos[i + 1]))
            i += 2
            continue
        i += 1
    ejecutar_con_depurador(ruta_archivo, breakpoints_iniciales)


if __name__ == "__main__":
    main()
