#
#   Imandra Inc.
#
#   speclogician/demos/cmd_demo.py
#

from __future__ import annotations

from pathlib import Path

import typer
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.rule import Rule
from rich.text import Text

from speclogician.demos.loader import (
    discover_demos,
    load_demo,
    load_changes,
)
from speclogician.demos.runner import apply_demo_changes
from speclogician.state.state import State
from speclogician.state.change import StateChange
from speclogician.utils import console

app = typer.Typer(help="Browse and run SpecLogician demos (markdown + changes + launch).")

DEFAULT_DEMOS_DIR = Path("demos").resolve()
CACHE_FILENAME = "state.json"


# -----------------------------------------------------------------------------
# helpers
# -----------------------------------------------------------------------------

def _cache_path(demo_root: Path) -> Path:
    return demo_root / CACHE_FILENAME

def _load_cached_state(cache_path: Path) -> State:
    data = cache_path.read_text(encoding="utf-8", errors="replace")
    # State is assumed to be a Pydantic model (v2): model_validate_json exists.
    return State.model_validate_json(data)  # type: ignore[attr-defined]


def _save_cached_state(state: State, cache_path: Path) -> None:
    cache_path.write_text(
        state.model_dump_json(indent=2),  # type: ignore[attr-defined]
        encoding="utf-8",
    )


def _build_state_from_changes(
        *, 
        demo_root: Path, 
        changes: list[StateChange], 
        console: Console
    ) -> State:
    res = apply_demo_changes(
        changes=changes,
        console=console,
        initial_state=None,
        show_progress=True,
    )
    return res.state


def _load_description(demo_root: Path) -> str:
    """ 
    """
    return (demo_root / "description.md").read_text()

def _ensure_cache_exists(
        *, 
        demo_root: Path, 
        changes: list[StateChange], 
        console: Console
    ) -> Path:
    """
    Cache policy:
    - demos run <name> : ALWAYS rebuild and overwrite cache
    - demos shell/tui  : REQUIRE cache; if missing, we build it once (quietly) and save it
    """
    cache_path = _cache_path(demo_root)
    if cache_path.exists():
        return cache_path

    console.print(Text(f"Cache missing; building {cache_path.name}…", style="dim"))
    res = apply_demo_changes(
        changes=changes,
        console=console,
        initial_state=None,
        show_progress=False,
    )
    _save_cached_state(res.state, cache_path)
    return cache_path

# -----------------------------------------------------------------------------
# commands
# -----------------------------------------------------------------------------

@app.command("ls")
def ls_cmd():
    demos = discover_demos(DEFAULT_DEMOS_DIR)
    if not demos:
        console.print(Text(f"No demos found in {DEFAULT_DEMOS_DIR}", style="yellow"))
        raise typer.Exit(0)

    console.print('[bold magenta italic]Available demos:')
    for d in demos:
        cache_path = _cache_path(d.root)
        description = _load_description(d.root).strip()
        cache_badge = "[green]cached[/green]" if cache_path.exists() else "[dim]no-cache[/dim]"
        console.print(f"• [bold]{d.name}[/bold]   {cache_badge}   [italic]{description}[/italic]")


@app.command("artifacts")
def artifacts_cmd(
    name: str = typer.Argument(..., help="Demo name (directory under demos/).")
):
    
    try:
        demo = load_demo(DEFAULT_DEMOS_DIR, name)
    except Exception as e:
        console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
        raise typer.Exit(2)

    artifact_path = demo.artifacts_path
    if not artifact_path.exists():
        console.print(Text(f"No artifact.md at {artifact_path}", style="yellow"))
        raise typer.Exit(1)

    text = artifact_path.read_text(encoding="utf-8", errors="replace")
    console.print(Panel(Text(f"{demo.name}", style="bold"), title="Demo", border_style="magenta"))
    console.print(Markdown(text))


@app.command("changes")
def changes_cmd(
    name: str = typer.Argument(..., help="Demo name."),
    show_meta: bool = typer.Option(True, "--meta/--no-meta", help="Show per-change rationale Markdown."),
):
    try:
        demo = load_demo(DEFAULT_DEMOS_DIR, name)
    except Exception as e:
        console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
        raise typer.Exit(2)

    changes: list[StateChange] = load_changes(demo)

    console.print(Panel(Text(demo.name, style="bold"), title="Demo changes", border_style="magenta"))
    console.print(Text(f"{len(changes)} changes", style="dim"))
    console.print()

    for i, ch in enumerate(changes, start=1):
        kind = getattr(ch, "kind", ch.__class__.__name__)
        console.print(Rule(f"[bold]{i}. {kind}[/bold]", style="magenta"))

        # Your StateChange implements __rich__ nicely.
        console.print(ch)

        if show_meta:
            meta = getattr(ch, "meta", None)
            if isinstance(meta, str) and meta.strip():
                console.print()
                console.print(Panel(Markdown(meta), title="rationale", border_style="cyan"))

        console.print()


@app.command("run")
def run_cmd(
    name: str = typer.Argument(..., help="Demo name."),
):
    """
    Run the demo from scratch (apply all changes) and overwrite demo/state.json.
    """
    try:
        demo = load_demo(DEFAULT_DEMOS_DIR, name)
    except Exception as e:
        console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
        raise typer.Exit(2)

    changes: list[StateChange] = load_changes(demo)

    console.print(Panel(Text(demo.name, style="bold"), title="Running demo", border_style="magenta"))
    state = _build_state_from_changes(demo_root=demo.root, changes=changes, console=console)

    cache_path = _cache_path(demo.root)
    _save_cached_state(state, cache_path)
    console.print(Text(f"Saved cache: {cache_path}", style="dim"))

    console.print(Rule("[bold]Result[/bold]", style="magenta"))

    insts = getattr(state, "instances", None) or []
    if insts:
        latest = insts[0]
        console.print(Text("Latest instance:", style="bold"))

        # ---- Presentation layer (summary panel) ----
        try:
            from speclogician.presentation.ctx import RenderCtx
            from speclogician.presentation.builders.state_instance_summary import (
                build_state_instance_summary_pm,
            )
            from speclogician.presentation.renderers.state_instance_summary import (
                render_state_instance_summary,
            )

            ctx = RenderCtx(
                art_cont=getattr(latest, "art_container", None),
                art_map=getattr(latest, "art_map", None),
            )
            pm = build_state_instance_summary_pm(latest, ctx)
            console.print(render_state_instance_summary(pm, ctx=ctx))
        except Exception as e:
            # If presentation breaks, don’t break demos; fallback to raw rich/repr.
            console.print(Text(f"[dim]Summary renderer failed: {e}[/dim]"))
            console.print(latest.__rich__() if hasattr(latest, "__rich__") else repr(latest))

        return

    console.print(Text("State applied.", style="yellow"))

@app.command('shell')
def shell_cmd(
    name: str = typer.Argument(..., help='Demo name.'),
):
    """
    Start an interactive Python shell using the cached demo state (demo/state.json).
    If missing, it will be built once and saved.
    """
    try:
        demo = load_demo(DEFAULT_DEMOS_DIR, name)
    except Exception as e:
        console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
        raise typer.Exit(2)

    changes: list[StateChange] = load_changes(demo)

    cache_path = _ensure_cache_exists(demo_root=demo.root, changes=changes, console=console)
    state = _load_cached_state(cache_path)

    import code
    banner = f"SpecLogician demo '{name}' loaded from cache. `state` is available."
    code.interact(banner=banner, local={"state": state})


@app.command("tui")
def tui_cmd(
    name: str = typer.Argument(..., help='Demo name.'),
):
    """
    Launch the Textual TUI using the cached demo state (demo/state.json).
    If missing, it will be built once and saved.
    """
    try:
        demo = load_demo(DEFAULT_DEMOS_DIR, name)
    except Exception as e:
        console.print(f'Failed to load in the demo: {str(e)}', style="bold red")
        raise typer.Exit(2)
    
    artifact_path = demo.artifacts_path

    if not artifact_path.exists():
        console.print(Text(f'No artifact.md at {artifact_path}', style="yellow"))
        raise typer.Exit(1)

    artifact_cmd = artifact_path.read_text(encoding='utf-8', errors='replace')

    changes: list[StateChange] = load_changes(demo)

    cache_path = _ensure_cache_exists(demo_root=demo.root, changes=changes, console=console)
    state = _load_cached_state(cache_path)

    from speclogician.tui.main_screen import SpecLogicianApp
    SpecLogicianApp(state=state, demo_md=artifact_cmd, demo_title=f'Demo: {name}').run()
