"""bandito observe — project listing, trace listing, trace inspection."""

from __future__ import annotations

import json as _json
from datetime import datetime, timezone
from typing import Any


def list_projects(store: Any) -> None:
    projects = store.list_projects()

    if not projects:
        print("\n  No projects found.")
        print("\n  Instrument your app with bandito.trace() first.\n")
        return

    print(f"\n  {len(projects)} project{'s' if len(projects) != 1 else ''}\n")

    for name in projects:
        summaries = store.list_trace_summaries(name, limit=100)
        trace_count = store.count_traces(name)
        total_cost = 0.0
        has_cost = False
        for t in summaries:
            c = t.get("total_cost")
            if c is not None and c > 0:
                total_cost += c
                has_cost = True

        parts = [f"{trace_count} traces"]
        if has_cost:
            parts.append(f"${total_cost:.4f}")

        print(f"  {name}")
        print(f"    {' \u00b7 '.join(parts)}")

    print()
    print("  Use --project to target a specific project:")
    print("    bandito diagnostics --project <name>")
    print("    bandito observe traces --project <name>")
    print("    bandito tui --project <name>")
    print()


def list_traces(
    store: Any,
    project: str | None,
    tag: str | None = None,
    last: int | None = 10,
    client: Any = None,
    json_output: bool = False,
) -> None:
    if project is None:
        projects = store.list_projects()
        if not projects:
            if json_output:
                import json as json_mod
                print(json_mod.dumps({"traces": [], "project": None}))
            else:
                print("\n  No traces found.")
                print("\n  Instrument your app with bandito.trace() first.\n")
            return
        if len(projects) == 1:
            project = projects[0]
        else:
            if json_output:
                import json as json_mod
                print(json_mod.dumps({"error": "multiple_projects", "projects": projects[:20]}))
            else:
                print(f"\n  {len(projects)} projects found. Pick one with --project:\n")
                for p in projects[:5]:
                    print(f"    bandito observe traces --project {p}")
                if len(projects) > 5:
                    print("    ...")
                print(f"\n  See all projects:  bandito observe projects\n")
            return

    effective_limit = last if last and last > 0 else None

    if json_output:
        traces = store.list_traces(project, tag=tag, limit=effective_limit)
        if not traces:
            import json as json_mod
            print(json_mod.dumps({"traces": [], "project": project}))
            return
        evals = client.list_evaluations(project) if client else {}
        _list_traces_json(traces, evals, project or "")
        return

    total_summaries = store.list_trace_summaries(project, tag=tag)
    if not total_summaries:
        print(f"\n  No traces for project '{project}'.\n")
        return

    if effective_limit:
        traces = total_summaries[:effective_limit]
    else:
        traces = total_summaries

    evals = client.list_evaluations(project) if client else {}

    total_count = len(total_summaries)
    showing = len(traces)
    if showing < total_count:
        print(f"\n  {total_count} traces \u00b7 {project} \u00b7 showing last {showing}\n")
    else:
        print(f"\n  {total_count} traces \u00b7 {project}\n")

    shown = traces
    for idx, t in enumerate(shown):
        _print_trace_row(t, evals, idx < len(shown) - 1)

    if showing < total_count:
        remaining = total_count - showing
        print(f"\n  {remaining} more traces. Use --last {total_count} or --last 0 to see all.")

    first_id = shown[0].get("trace_id", "<id>") if shown else "<id>"
    print(f"\n  Inspect a trace:")
    print(f"    bandito observe trace {first_id}")
    print()


def show_trace(store: Any, trace_id: str, client: Any = None) -> None:
    trace = store.get_trace(trace_id)

    if trace is None:
        print(f"\n  Trace '{trace_id}' not found.\n")
        return

    trace_eval = client.get_evaluation(trace_id) if client else None

    print(f"\n  trace {trace_id}\n")

    project = trace.get("project", "?")
    status = trace.get("status", "ok")
    print(f"  Project   {project}")
    print(f"  Status    {status}")

    started = trace.get("started_at", "")
    ended = trace.get("ended_at", "")
    if started:
        print(f"  Started   {started}")
    if started and ended:
        try:
            s = datetime.fromisoformat(started.replace("Z", "+00:00"))
            e = datetime.fromisoformat(ended.replace("Z", "+00:00"))
            secs = (e - s).total_seconds()
            latency = f"{secs:.2f}s" if secs < 60 else f"{secs / 60:.1f}m"
            print(f"  Latency   {latency}")
        except (ValueError, TypeError):
            pass

    inp = trace.get("input")
    out = trace.get("output")
    if inp is not None:
        print(f"\n  \u2500\u2500 Input {'\u2500' * 52}")
        _print_wrapped(inp, indent=4)
    if out is not None:
        print(f"\n  \u2500\u2500 Output {'\u2500' * 51}")
        _print_wrapped(out, indent=4)

    spans = trace.get("spans", [])
    if spans:
        print(f"\n  \u2500\u2500 Spans ({len(spans)}) {'\u2500' * (46 - len(str(len(spans))))}")
        span_by_id = {s.get("span_id"): s for s in spans}
        printed: set[str] = set()
        _print_span_tree(spans, span_by_id, parent_id=None, depth=0, printed=printed)

    if trace_eval:
        print(f"\n  \u2500\u2500 Evaluations {'─' * 46}")
        if trace_eval.get("human_score") is not None:
            score = trace_eval["human_score"]
            mark = "\u2713" if score == 1 else "\u2717"
            parts = [f"{mark} score={score}", "source=human"]
            span_ids = trace_eval.get("human_span_ids")
            if span_ids:
                parts.append(f"spans={','.join(span_ids[:3])}")
            comment = trace_eval.get("human_comment")
            if comment:
                parts.append(f'"{comment}"')
            print(f"    {' \u00b7 '.join(parts)}")
        if trace_eval.get("judge_score") is not None:
            score = trace_eval["judge_score"]
            mark = "\u2713" if score == 1 else "\u2717"
            parts = [f"{mark} score={score}", "source=judge"]
            critique = trace_eval.get("judge_critique")
            if critique:
                parts.append(f'"{critique[:60]}"')
            print(f"    {' \u00b7 '.join(parts)}")

    if trace.get("error"):
        print(f"\n  \u2500\u2500 Error {'\u2500' * 52}")
        print(f"    {trace['error']}")

    print()


# -- Helpers -------------------------------------------------------------------


def _content_preview(value: Any, max_len: int = 125) -> str:
    """Format a trace input/output value as a one-line preview."""
    from ..storage.summary import content_preview
    return content_preview(value, max_len=max_len)


def format_ago(timestamp_str: str) -> str:
    """Format an ISO timestamp as a human-readable 'Xh ago' string."""
    try:
        dt = datetime.fromisoformat(str(timestamp_str).replace("Z", "+00:00"))
        delta = datetime.now(timezone.utc) - dt
        total_secs = int(delta.total_seconds())
        if total_secs < 0:
            return "just now"
        elif total_secs < 60:
            return f"{total_secs}s ago"
        elif total_secs < 3600:
            return f"{total_secs // 60}m ago"
        elif total_secs < 86400:
            return f"{total_secs // 3600}h ago"
        else:
            return f"{total_secs // 86400}d ago"
    except (ValueError, TypeError):
        return str(timestamp_str)[:16]


def _list_traces_json(traces: list[dict], evals: dict, project: str) -> None:
    import json as json_mod

    output = []
    for t in traces:
        tid = t.get("trace_id", "?")
        trace_eval = evals.get(tid)
        entry = {
            "trace_id": tid,
            "project": project,
            "status": t.get("status", "ok"),
            "input": t.get("input"),
            "output": t.get("output"),
            "started_at": t.get("started_at"),
            "ended_at": t.get("ended_at"),
            "span_count": len(t.get("spans", [])),
            "tags": t.get("tags", []),
        }
        cost = sum(s.get("cost", 0) or 0 for s in t.get("spans", []))
        if cost > 0:
            entry["cost"] = cost
        if trace_eval:
            entry["evaluation"] = {
                "human_score": trace_eval.get("human_score"),
                "human_comment": trace_eval.get("human_comment"),
                "judge_score": trace_eval.get("judge_score"),
                "judge_critique": trace_eval.get("judge_critique"),
            }
        output.append(entry)
    print(json_mod.dumps({"traces": output, "project": project}, indent=2, default=str))


def _print_trace_row(t: dict, evals: dict, show_separator: bool) -> None:
    """Print a trace row. Works with both full traces and summaries."""
    tid = t.get("trace_id", "?")
    status = t.get("status", "ok")

    # Summary path: use pre-computed fields
    span_count = t.get("span_count", len(t.get("spans", [])))
    cost = t.get("total_cost")
    has_cost = cost is not None
    if not has_cost and "spans" in t:
        cost = sum(s.get("cost") or 0 for s in t["spans"] if s.get("cost") is not None)
        has_cost = cost > 0

    latency = t.get("latency")
    if latency is None:
        started = t.get("started_at", "")
        ended = t.get("ended_at", "")
        if started and ended:
            try:
                s = datetime.fromisoformat(started.replace("Z", "+00:00"))
                e = datetime.fromisoformat(ended.replace("Z", "+00:00"))
                latency = (e - s).total_seconds()
            except (ValueError, TypeError):
                pass
    latency_str = ""
    if latency is not None:
        latency_str = f"{latency:.2f}s" if latency < 60 else f"{latency / 60:.1f}m"

    started = t.get("started_at", "")
    ago_str = format_ago(started) if started else ""

    trace_eval = evals.get(tid)
    if trace_eval and trace_eval.get("human_score") is not None:
        grade_str = "graded \u2713" if trace_eval["human_score"] == 1 else "graded \u2717"
    else:
        grade_str = "- grade"

    judge_str = ""
    if trace_eval and trace_eval.get("judge_score") is not None:
        judge_str = "judge \u2713" if trace_eval["judge_score"] == 1 else "judge \u2717"

    parts = []
    if ago_str:
        parts.append(ago_str)
    parts.append(f"{span_count} spans")
    if has_cost:
        parts.append(f"${cost:.4f}")
    if latency_str:
        parts.append(latency_str)
    if status == "error":
        parts.append("ERROR")
    parts.append(grade_str)
    if judge_str:
        parts.append(judge_str)

    print(f"  {tid}   {' \u00b7 '.join(parts)}")

    # Summary path: use pre-computed preview. Full trace: compute on the fly.
    inp_preview = t.get("input_preview") or (
        _content_preview(t["input"]) if t.get("input") else ""
    )
    if inp_preview:
        print(f"  input: {inp_preview}")

    out_preview = t.get("output_preview") or (
        _content_preview(t["output"]) if t.get("output") else ""
    )
    if out_preview:
        print(f"  output: {out_preview}")

    if show_separator:
        print(f"  {'─' * 60}")


def _print_span_tree(spans, span_by_id, parent_id, depth, printed):
    for span in spans:
        sid = span.get("span_id")
        if sid in printed:
            continue
        if span.get("parent_id") != parent_id:
            continue
        printed.add(sid)

        indent = "    " + "  " * depth
        kind = span.get("kind", "?")
        name = span.get("name", "?")
        status = span.get("status", "ok")

        parts = [f"{kind}:{name}"]

        if span.get("model"):
            parts.append(span["model"])

        s = span.get("started_at", "")
        e = span.get("ended_at", "")
        if s and e:
            try:
                st = datetime.fromisoformat(s.replace("Z", "+00:00"))
                et = datetime.fromisoformat(e.replace("Z", "+00:00"))
                secs = (et - st).total_seconds()
                parts.append(f"{secs:.2f}s" if secs < 60 else f"{secs / 60:.1f}m")
            except (ValueError, TypeError):
                pass

        if span.get("cost") is not None:
            parts.append(f"${span['cost']:.4f}")

        tokens = []
        if span.get("input_tokens"):
            tokens.append(f"{span['input_tokens']}in")
        if span.get("output_tokens"):
            tokens.append(f"{span['output_tokens']}out")
        if tokens:
            parts.append("+".join(tokens))

        if status == "error":
            err = span.get("error", "error")
            parts.append(f"ERROR: {err}")

        print(f"{indent}{' \u00b7 '.join(parts)}")

        _print_span_tree(spans, span_by_id, parent_id=sid, depth=depth + 1, printed=printed)


def _print_wrapped(value, indent=4):
    import json as _json
    prefix = " " * indent
    if isinstance(value, (dict, list)):
        text = _json.dumps(value, indent=2)
    else:
        text = str(value)
    for line in text.split("\n"):
        print(f"{prefix}{line}")
