#
#   Imandra Inc.
#
#   speclogician/commands/find_cmd.py
#

from __future__ import annotations

from typing import Any

import typer
from rich.table import Table

from .utils import get_state
from ..utils import console

app = typer.Typer(help="Find various components of the state")


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

def _matches_component(q: str, *, name: str, src_code: str) -> bool:
    qq = (q or "").lower()
    return (qq in (name or "").lower()) or (qq in (src_code or "").lower())


def _emit_json(payload: dict[str, Any]) -> None:
    # Keep stdout clean for machine mode (matches your convention elsewhere)
    import json as _json
    typer.echo(_json.dumps(payload, indent=2, sort_keys=True))


def _preview(src: str, n: int = 160) -> str:
    return (src or "")[:n].replace("\n", " ")


# -----------------------------------------------------------------------------
# predicates
# -----------------------------------------------------------------------------

@app.command(name="predicates", help="Search state/action predicates by name or source text")
def search_predicates(
    ctx: typer.Context,
    q: str = typer.Argument(..., help="Search string (name or source snippet)"),
    kind: str = typer.Option(
        "all",
        "--kind",
        help="Search scope: all|state|action",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
) -> None:
    state = get_state(ctx=ctx)
    dm = state.curr_state().spec.domain_model

    k = (kind or "all").lower().strip()
    if k not in ("all", "state", "action"):
        raise typer.BadParameter("kind must be one of: all|state|action")

    matches: list[dict[str, Any]] = []

    if k in ("all", "state"):
        for p in getattr(dm, "state_preds", []):
            name = getattr(p, "name", "")
            src_code = getattr(p, "src_code", "")
            if _matches_component(q, name=name, src_code=src_code):
                matches.append(
                    {
                        "type": "StatePredicate",
                        "kind": getattr(p, "kind", "state_predicate"),
                        "name": name,
                        "src_preview": _preview(src_code),
                    }
                )

    if k in ("all", "action"):
        for p in getattr(dm, "action_preds", []):
            name = getattr(p, "name", "")
            src_code = getattr(p, "src_code", "")
            if _matches_component(q, name=name, src_code=src_code):
                matches.append(
                    {
                        "type": "ActionPredicate",
                        "kind": getattr(p, "kind", "action_predicate"),
                        "name": name,
                        "src_preview": _preview(src_code),
                    }
                )

    # Deterministic output (nice for tests)
    matches.sort(key=lambda m: (m.get("type", ""), m.get("name", "")))

    if json_only:
        _emit_json(
            {
                "query": q,
                "kind": k,
                "count": len(matches),
                "results": matches,
            }
        )
        return

    t = Table(title=f"Predicate search for {q!r}", show_header=True, header_style="bold")
    t.add_column("Type", no_wrap=True)
    t.add_column("Name", no_wrap=True)
    t.add_column("Preview")
    for m in matches:
        prev = m["src_preview"]
        t.add_row(m["type"], m["name"], prev + ("…" if len(prev) == 160 else ""))
    console.print(t)


# -----------------------------------------------------------------------------
# transitions
# -----------------------------------------------------------------------------

@app.command(name="transitions", help="Search transition functions by name or source text")
def search_transitions(
    ctx: typer.Context,
    q: str = typer.Argument(..., help="Search string (name or source snippet)"),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
) -> None:
    state = get_state(ctx=ctx)
    dm = state.curr_state().spec.domain_model

    matches: list[dict[str, Any]] = []

    for tr in getattr(dm, "transitions", []):
        name = getattr(tr, "name", "")
        src_code = getattr(tr, "src_code", "")
        if _matches_component(q, name=name, src_code=src_code):
            matches.append(
                {
                    "type": "Transition",
                    "kind": getattr(tr, "kind", "transition"),
                    "name": name,
                    "src_preview": _preview(src_code),
                }
            )

    matches.sort(key=lambda m: m.get("name", ""))

    if json_only:
        _emit_json(
            {
                "query": q,
                "count": len(matches),
                "results": matches,
            }
        )
        return

    t = Table(title=f"Transition search for {q!r}", show_header=True, header_style="bold")
    t.add_column("Name", no_wrap=True)
    t.add_column("Preview")
    for m in matches:
        prev = m["src_preview"]
        t.add_row(m["name"], prev + ("…" if len(prev) == 160 else ""))
    console.print(t)


# -----------------------------------------------------------------------------
# scenarios
# -----------------------------------------------------------------------------

def _matches_scenario(q: str, scenario: Any) -> bool:
    qq = (q or "").lower()
    name = getattr(scenario, "name", "")
    if qq in (name or "").lower():
        return True
    for section in ("given", "when", "then"):
        for comp in getattr(scenario, section, []):
            if qq in (comp or "").lower():
                return True
    return False


@app.command(name="scenarios", help="Search scenarios by name or component membership (predicate/transition names)")
def search_scenarios(
    ctx: typer.Context,
    q: str = typer.Argument(..., help="Search string (scenario name, predicate name, or transition name)"),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
) -> None:
    state = get_state(ctx=ctx)
    scenarios = getattr(state.curr_state().spec, "scenarios", [])

    matches: list[dict[str, Any]] = []

    for sc in scenarios:
        if _matches_scenario(q, sc):
            matches.append(
                {
                    "name": getattr(sc, "name", ""),
                    "is_initial": getattr(sc, "is_initial", False),
                    "given": list(getattr(sc, "given", [])),
                    "when": list(getattr(sc, "when", [])),
                    "then": list(getattr(sc, "then", [])),
                }
            )

    matches.sort(key=lambda m: m.get("name", ""))

    if json_only:
        _emit_json(
            {
                "query": q,
                "count": len(matches),
                "results": matches,
            }
        )
        return

    t = Table(title=f"Scenario search for {q!r}", show_header=True, header_style="bold")
    t.add_column("Name", no_wrap=True)
    t.add_column("Initial", no_wrap=True)
    t.add_column("Given")
    t.add_column("When")
    t.add_column("Then")
    for m in matches:
        t.add_row(
            m["name"],
            "yes" if m["is_initial"] else "",
            ", ".join(m["given"]),
            ", ".join(m["when"]),
            ", ".join(m["then"]),
        )
    console.print(t)


# -----------------------------------------------------------------------------
# usages (reverse lookup)
# -----------------------------------------------------------------------------

@app.command(name="usages", help="Find which scenarios and artifacts reference a given component (predicate or transition)")
def search_usages(
    ctx: typer.Context,
    component: str = typer.Argument(..., help="Exact component name (predicate or transition)"),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
) -> None:
    state = get_state(ctx=ctx)
    inst = state.curr_state()
    spec = inst.spec
    dm = spec.domain_model
    art_map = inst.art_map

    # Determine what kind of component this is
    comp_type: str | None = None
    for p in getattr(dm, "state_preds", []):
        if getattr(p, "name", "") == component:
            comp_type = "state_predicate"
            break
    if comp_type is None:
        for p in getattr(dm, "action_preds", []):
            if getattr(p, "name", "") == component:
                comp_type = "action_predicate"
                break
    if comp_type is None:
        for tr in getattr(dm, "transitions", []):
            if getattr(tr, "name", "") == component:
                comp_type = "transition"
                break

    # Find scenarios that use this component
    scenario_usages: list[dict[str, Any]] = []
    for sc in getattr(spec, "scenarios", []):
        sections: list[str] = []
        if component in list(getattr(sc, "given", [])):
            sections.append("given")
        if component in list(getattr(sc, "when", [])):
            sections.append("when")
        if component in list(getattr(sc, "then", [])):
            sections.append("then")
        if sections:
            scenario_usages.append({
                "scenario": getattr(sc, "name", ""),
                "sections": sections,
            })
    scenario_usages.sort(key=lambda m: m["scenario"])

    # Find linked artifacts
    linked_art_ids = sorted(art_map.get_arts_for_component(component))

    if json_only:
        _emit_json(
            {
                "component": component,
                "component_type": comp_type,
                "scenarios": scenario_usages,
                "scenario_count": len(scenario_usages),
                "linked_artifact_ids": linked_art_ids,
                "linked_artifact_count": len(linked_art_ids),
            }
        )
        return

    # Rich output
    if comp_type:
        console.print(f"[bold]{component}[/bold] ({comp_type})\n")
    else:
        console.print(f"[bold]{component}[/bold] (not found in domain model)\n")

    if scenario_usages:
        t = Table(title="Scenarios using this component", show_header=True, header_style="bold")
        t.add_column("Scenario", no_wrap=True)
        t.add_column("Sections")
        for u in scenario_usages:
            t.add_row(u["scenario"], ", ".join(u["sections"]))
        console.print(t)
    else:
        console.print("[dim]No scenarios reference this component.[/dim]")

    if linked_art_ids:
        console.print(f"\n[bold]Linked artifacts:[/bold] {', '.join(linked_art_ids)}")
    else:
        console.print("\n[dim]No artifacts linked to this component.[/dim]")


# -----------------------------------------------------------------------------
# artifacts (smart_find + PM renderer)
# -----------------------------------------------------------------------------

@app.command("artifacts")
def search_artifacts(
    ctx: typer.Context,
    q: str = typer.Argument(..., help="Search string"),
    kind: str = typer.Option(
        "all",
        "--kind",
        help="Search scope: all|tests|logs|docs|src",
    ),
    json_only: bool = typer.Option(
        False,
        "--json",
        help="Emit JSON only (no rich output)",
    ),
    ids: bool = typer.Option(
        False,
        "--ids",
        help="Show artifact IDs in rich output",
    ),
    compact: bool = typer.Option(
        False,
        "--compact",
        help="Use compact tables/panels in rich output",
    ),
    show_empty: bool = typer.Option(
        False,
        "--show-empty",
        help="Render empty sections in rich output",
    ),
    max_per_type: int = typer.Option(
        50,
        "--max-per-type",
        min=1,
        help="Maximum results to show per artifact type",
    ),
) -> None:
    """Smart text search across artifacts (tests/logs/docs/src)."""
    state = get_state(ctx=ctx)
    ac = state.curr_state().art_container

    k = (kind or "all").lower().strip()
    include: set[str] | None
    if k in ("all", "*"):
        include = None
    elif k in ("tests", "test"):
        include = {"test"}
    elif k in ("logs", "log"):
        include = {"log"}
    elif k in ("docs", "doc"):
        include = {"doc"}
    elif k in ("src", "source"):
        include = {"src"}
    else:
        raise typer.BadParameter("kind must be one of: all|tests|logs|docs|src")

    # --- search ---
    res = ac.smart_find(q, include=include, max_per_type=max_per_type)

    # --- build PM + render ---
    from speclogician.presentation.ctx import RenderCtx
    from speclogician.presentation.builders.smart_find import build_smart_find_pm
    from speclogician.presentation.renderers.smart_find import render_smart_find

    rctx = RenderCtx(
        show_art_ids=ids,
        show_ids=ids,
        compact=compact,
        show_empty_sections=show_empty,  # now a real field on RenderCtx
    )

    pm = build_smart_find_pm(res, rctx)

    if json_only:
        payload = pm.model_dump()
        # test-friendly extra metadata
        payload["query"] = q
        payload["kind"] = k
        _emit_json(payload)
        return

    console.print(render_smart_find(pm, ctx=rctx))