"""cc-logger CLI — export and analyse Claude Code sessions."""

from __future__ import annotations

import json
import logging
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Annotated, Optional

import typer
from rich.console import Console
from rich.table import Table

from cc_logger.discovery.filters import apply_filters
from cc_logger.discovery.projects import scan_projects
from cc_logger.discovery.subagents import discover_subagents
from cc_logger.mapper.trajectory import export_session
from cc_logger.pricing import calculate_cost, load_pricing, scrape_pricing, save_pricing

app = typer.Typer(help="Export and analyse Claude Code sessions.")
console = Console()

_claude_dir: Path = Path.home() / ".claude"


@app.callback()
def _callback(
    claude_dir: Annotated[Path, typer.Option(help="Claude data dir")] = Path.home() / ".claude",
    verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
) -> None:
    global _claude_dir
    _claude_dir = claude_dir
    logging.basicConfig(
        level=logging.DEBUG if verbose else logging.WARNING,
        format="%(levelname)s: %(message)s",
    )


# ── Commands ─────────────────────────────────────────────────────────────


@app.command("list")
def list_sessions(
    project: Annotated[Optional[str], typer.Option(help="Filter by project path")] = None,
    since: Annotated[Optional[str], typer.Option(help="YYYY-MM-DD")] = None,
) -> None:
    """List available sessions."""
    sessions = _get_sessions(project=project, since=since)
    if not sessions:
        typer.echo("No sessions found")
        return

    sessions.sort(key=lambda s: s.modified or "", reverse=True)
    typer.echo(f"{'SESSION':10} {'PROJECT':30} {'DATE':12} {'MSGS':>5} {'FIRST PROMPT'}")
    typer.echo("-" * 90)
    for s in sessions:
        typer.echo(
            f"{s.session_id[:8]:10} "
            f"{s.project_path[-30:]:30} "
            f"{(s.modified or s.created or '')[:10]:12} "
            f"{s.message_count:>5} "
            f"{(s.first_prompt or '')[:60].replace(chr(10), ' ')}"
        )
    typer.echo(f"\n{len(sessions)} sessions")


@app.command()
def export(
    output_dir: Annotated[Path, typer.Argument(help="Output directory")],
    project: Annotated[Optional[str], typer.Option(help="Filter by project path")] = None,
    session_id: Annotated[Optional[str], typer.Option("--session", help="Single session UUID/prefix")] = None,
    since: Annotated[Optional[str], typer.Option(help="YYYY-MM-DD")] = None,
    harbor: Annotated[bool, typer.Option(help="Harbor viewer layout")] = False,
    job_name: Annotated[Optional[str], typer.Option(help="Override Harbor job name")] = None,
) -> None:
    """Export sessions as ATIF trajectory files."""
    sessions = _get_sessions(project=project, since=since, session_id=session_id)
    if not sessions:
        typer.echo("No sessions found matching filters")
        return

    try:
        output_dir.mkdir(parents=True, exist_ok=True)
    except OSError as e:
        typer.echo(f"Error: cannot create output directory: {e}", err=True)
        sys.exit(1)

    total, exported, failed = len(sessions), 0, 0
    # Track per-job trial metadata for writing job-level files
    job_trials: dict[str, list[dict]] = {}

    for i, session in enumerate(sessions, 1):
        try:
            subagents = discover_subagents(session.session_id, session.jsonl_path.parent)
            out_path = _resolve_output_path(output_dir, session, harbor, job_name, total, session_id)
            out_path.parent.mkdir(parents=True, exist_ok=True)

            trajectory = export_session(
                jsonl_path=session.jsonl_path, subagents=subagents, output_dir=out_path.parent,
            )
            out_path.write_text(json.dumps(trajectory, indent=2))

            if harbor:
                effective_job = job_name or _clean_project_name(session.project_path)
                trial_result = _write_harbor_result(output_dir / effective_job / session.session_id, trajectory, session)
                job_trials.setdefault(effective_job, []).append(trial_result)

            exported += 1
            step_count = len(trajectory.get("steps", []))

            if total == 1:
                fm = trajectory.get("final_metrics", {})
                typer.echo(f"Exported to {out_path}")
                typer.echo(f"  Steps: {step_count}")
                typer.echo(f"  Prompt tokens: {fm.get('total_prompt_tokens', 0):,}")
                typer.echo(f"  Completion tokens: {fm.get('total_completion_tokens', 0):,}")
            else:
                typer.echo(f"[{i}/{total}] Exported {session.session_id[:8]} ({step_count} steps)")

        except Exception as e:
            failed += 1
            typer.echo(f"[{i}/{total}] FAILED {session.session_id[:8]}: {e}", err=True)
            logging.getLogger(__name__).debug("Export failed", exc_info=True)

    # Write job-level config.json + result.json so Harbor shows metadata columns
    if harbor:
        for jname, trials in job_trials.items():
            _write_harbor_job_metadata(output_dir / jname, trials)

    if total > 1:
        typer.echo(f"\nExported {exported}/{total} sessions to {output_dir}")
        if failed:
            typer.echo(f"  {failed} sessions failed")
    if harbor and exported > 0:
        typer.echo(f"\nRun: cc-logger view {output_dir}")


@app.command()
def stats(
    export_dir: Annotated[Path, typer.Argument(help="Harbor export directory")],
    output_json: Annotated[bool, typer.Option("--json", help="JSON output")] = False,
    project: Annotated[Optional[str], typer.Option(help="Filter by project")] = None,
) -> None:
    """Show analytics for exported sessions."""
    sessions = _load_results(export_dir, project)
    if not sessions:
        typer.echo(f"No result.json files found in {export_dir}")
        raise typer.Exit(1)

    agg = _aggregate(sessions)

    if output_json:
        typer.echo(json.dumps(agg, indent=2))
    else:
        _print_stats(agg)


@app.command()
def view(
    export_dir: Annotated[Path, typer.Argument(help="Harbor export directory")],
    port: Annotated[str, typer.Option("-p", "--port")] = "8080-8089",
    host: Annotated[str, typer.Option(help="Host to bind to")] = "127.0.0.1",
) -> None:
    """Open Harbor trajectory viewer."""
    try:
        subprocess.run(["harbor", "view", str(export_dir), "--port", port, "--host", host], check=True)
    except FileNotFoundError:
        typer.echo("Error: 'harbor' not found. Install with: pip install harbor", err=True)
        raise typer.Exit(1)
    except KeyboardInterrupt:
        pass


@app.command("update-pricing")
def update_pricing() -> None:
    """Scrape latest model pricing from Anthropic."""
    typer.echo("Fetching pricing from platform.claude.com...")
    try:
        pricing = scrape_pricing()
    except Exception as e:
        typer.echo(f"Error: {e}", err=True)
        raise typer.Exit(1)

    path = save_pricing(pricing)
    models = pricing.get("models", {})
    typer.echo(f"Saved {len(models)} models to {path}")
    typer.echo(f"Updated: {pricing.get('updated_at')}")
    for name, p in sorted(models.items()):
        typer.echo(f"  {name:25s}  in=${p['input']:<6}  out=${p['output']:<6}  cache=${p['cache_read']}")


# ── Helpers ──────────────────────────────────────────────────────────────


def _get_sessions(*, project=None, since=None, session_id=None):
    """Discover and filter sessions from the Claude data directory."""
    projects = scan_projects(_claude_dir)
    all_sessions = [s for p in projects for s in p.sessions]
    return apply_filters(all_sessions, project=project, since=since, session_id=session_id)


def _resolve_output_path(output_dir, session, harbor, job_name, total, session_id):
    """Determine the output file path for a session."""
    if harbor:
        job = job_name or _clean_project_name(session.project_path)
        return output_dir / job / session.session_id / "agent" / "trajectory.json"
    if total == 1 and session_id:
        return output_dir / f"{session.session_id}.json"
    return output_dir / session.project_slug / f"{session.session_id}.json"


def _write_harbor_result(trial_dir: Path, trajectory: dict, session) -> dict:
    """Write trial-level result.json for Harbor viewer. Returns the result dict."""
    agent_data = trajectory.get("agent", {})
    fm = trajectory.get("final_metrics", {})
    model_name = agent_data.get("model_name")
    provider = _infer_provider(model_name)

    started_at = finished_at = None
    for step in trajectory.get("steps", []):
        ts = step.get("timestamp")
        if ts:
            started_at = started_at or ts
            finished_at = ts

    result = {
        "task_name": (session.first_prompt or session.session_id)[:80].replace("\n", " "),
        "trial_name": session.session_id,
        "trial_uri": session.session_id,
        "task_id": {"path": session.project_path},
        "task_checksum": session.session_id[:8],
        "config": {
            "task": {"path": session.project_path},
            "agent": {"name": "claude-code", "model_name": f"{provider}/{model_name}" if model_name else None},
        },
        "agent_info": {
            "name": "claude-code",
            "version": agent_data.get("version", "unknown"),
            "model_info": {"name": model_name or "unknown", "provider": provider},
        },
        "agent_result": {
            "n_input_tokens": fm.get("total_prompt_tokens"),
            "n_output_tokens": fm.get("total_completion_tokens"),
            "n_cache_tokens": fm.get("total_cached_tokens"),
        },
        "started_at": started_at,
        "finished_at": finished_at,
        "agent_execution": {"started_at": started_at, "finished_at": finished_at},
    }
    (trial_dir / "result.json").write_text(json.dumps(result, indent=2))
    return result


def _write_harbor_job_metadata(job_dir: Path, trial_results: list[dict]) -> None:
    """Write job-level config.json and result.json so Harbor populates metadata columns."""
    # Collect unique models/providers from trials
    models = set()
    providers = set()
    timestamps = []
    for tr in trial_results:
        mi = (tr.get("agent_info") or {}).get("model_info") or {}
        if mi.get("name") and mi["name"] != "unknown":
            prov = mi.get("provider", "")
            models.add(f"{prov}/{mi['name']}" if prov and prov != "unknown" else mi["name"])
            if prov and prov != "unknown":
                providers.add(prov)
        for key in ("started_at", "finished_at"):
            if tr.get(key):
                timestamps.append(tr[key])

    started = min(timestamps) if timestamps else None
    finished = max(timestamps) if timestamps else None

    # config.json — Harbor reads agents[].model_name as "provider/model"
    config = {
        "job_name": job_dir.name,
        "agents": [{"name": "claude-code", "model_name": m} for m in sorted(models)],
        "environment": {"type": None},
        "datasets": [],
    }
    (job_dir / "config.json").write_text(json.dumps(config, indent=2))

    # result.json — Harbor reads started_at, finished_at, n_total_trials, stats
    result = {
        "id": str(__import__("uuid").uuid4()),
        "started_at": started,
        "finished_at": finished,
        "n_total_trials": len(trial_results),
        "stats": {"n_trials": len(trial_results), "n_errors": 0, "evals": {}},
    }
    (job_dir / "result.json").write_text(json.dumps(result, indent=2))


def _load_results(export_dir: Path, project: str | None) -> list[dict]:
    """Load all result.json files from a Harbor export directory."""
    results = []
    for rf in sorted(export_dir.glob("*/*/result.json")):
        try:
            data = json.loads(rf.read_text())
        except (json.JSONDecodeError, OSError):
            continue
        proj_path = (data.get("task_id") or {}).get("path", "")
        if project and project.lower() not in proj_path.lower():
            continue
        results.append(data)
    return results


def _aggregate(sessions: list[dict]) -> dict:
    """Aggregate stats from result.json sessions into a summary dict."""
    pricing = load_pricing()

    total_input = total_output = total_cached = 0
    total_cost = 0.0
    total_hours = 0.0
    unique_projects: set[str] = set()
    models: dict[str, int] = {}
    model_costs: dict[str, float] = {}
    providers: dict[str, int] = {}
    project_counts: dict[str, int] = {}
    durations: list[float] = []
    timestamps: list[str] = []

    for s in sessions:
        ar = s.get("agent_result") or {}
        inp = ar.get("n_input_tokens") or 0
        out = ar.get("n_output_tokens") or 0
        cached = ar.get("n_cache_tokens") or 0
        total_input += inp
        total_output += out
        total_cached += cached

        proj = (s.get("task_id") or {}).get("path", "unknown")
        unique_projects.add(proj)
        project_counts[proj] = project_counts.get(proj, 0) + 1

        mi = (s.get("agent_info") or {}).get("model_info") or {}
        model = mi.get("name", "unknown")
        prov = mi.get("provider", "unknown")
        models[model] = models.get(model, 0) + 1
        providers[prov] = providers.get(prov, 0) + 1

        # Cost calculation
        cost = calculate_cost(
            input_tokens=inp, output_tokens=out, cache_tokens=cached,
            model=model, pricing=pricing,
        )
        if cost is not None:
            total_cost += cost
            model_costs[model] = model_costs.get(model, 0.0) + cost

        started, finished = s.get("started_at"), s.get("finished_at")
        if started and finished:
            try:
                t0 = datetime.fromisoformat(started.replace("Z", "+00:00"))
                t1 = datetime.fromisoformat(finished.replace("Z", "+00:00"))
                dur_h = (t1 - t0).total_seconds() / 3600
                if dur_h >= 0:
                    total_hours += dur_h
                    durations.append(dur_h)
            except (ValueError, TypeError):
                pass
        for key in ("started_at", "finished_at"):
            if s.get(key):
                timestamps.append(s[key])

    n = len(sessions)
    return {
        "total_sessions": n,
        "unique_projects": len(unique_projects),
        "date_range": [min(timestamps)[:10] if timestamps else "?", max(timestamps)[:10] if timestamps else "?"],
        "tokens": {"input": total_input, "output": total_output, "cached": total_cached, "total": total_input + total_output},
        "avg_tokens_per_session": {"input": total_input // n if n else 0, "output": total_output // n if n else 0},
        "total_hours": round(total_hours, 2),
        "avg_session_hours": round(total_hours / len(durations), 2) if durations else 0,
        "cost": {"total": round(total_cost, 2), "avg_per_session": round(total_cost / n, 2) if n else 0, "by_model": {k: round(v, 2) for k, v in model_costs.items()}},
        "pricing_updated": pricing.get("updated_at", "unknown"),
        "models": models,
        "providers": providers,
        "sessions_per_project": project_counts,
    }


def _print_stats(agg: dict) -> None:
    """Render aggregated stats as rich tables."""
    n = agg["total_sessions"]
    tok = agg["tokens"]
    cost = agg.get("cost", {})
    console.print()

    t = Table(title="Session Summary", show_header=False, title_style="bold cyan")
    t.add_column("Metric", style="bold")
    t.add_column("Value", justify="right")
    t.add_row("Total sessions", str(n))
    t.add_row("Unique projects", str(agg["unique_projects"]))
    t.add_row("Date range", f"{agg['date_range'][0]}  to  {agg['date_range'][1]}")
    t.add_row("Total working hours", f"{agg['total_hours']:.1f} h")
    avg_min = agg["avg_session_hours"] * 60
    t.add_row("Avg session duration", f"{avg_min:.0f} min" if avg_min else "n/a")
    if cost.get("total"):
        t.add_row("Total cost", f"${cost['total']:,.2f}")
        t.add_row("Avg cost / session", f"${cost['avg_per_session']:,.2f}")
    console.print(t)
    console.print()

    t = Table(title="Token Usage", title_style="bold cyan")
    t.add_column("", style="bold")
    t.add_column("Total", justify="right")
    t.add_column("Avg / session", justify="right")
    avg = agg["avg_tokens_per_session"]
    t.add_row("Input", f"{tok['input']:,}", f"{avg['input']:,}")
    t.add_row("Output", f"{tok['output']:,}", f"{avg['output']:,}")
    t.add_row("Cached", f"{tok['cached']:,}", f"{tok['cached'] // n if n else 0:,}")
    t.add_row("Combined", f"{tok['total']:,}", f"{tok['total'] // n if n else 0:,}", style="bold")
    console.print(t)
    console.print()

    # Cost by model
    by_model = cost.get("by_model", {})
    if by_model:
        t = Table(title=f"Cost by Model  (pricing: {agg.get('pricing_updated', '?')})", title_style="bold cyan")
        t.add_column("Model", style="bold")
        t.add_column("Sessions", justify="right")
        t.add_column("Cost", justify="right")
        t.add_column("%", justify="right")
        total_c = cost.get("total", 1) or 1
        for name, c in sorted(by_model.items(), key=lambda x: -x[1]):
            sess_count = agg["models"].get(name, 0)
            t.add_row(name, str(sess_count), f"${c:,.2f}", f"{c / total_c * 100:.0f}%")
        console.print(t)
        console.print()

    for title, data in [("Provider Usage", agg["providers"])]:
        if data:
            t = Table(title=title, title_style="bold cyan")
            t.add_column(title.split()[0], style="bold")
            t.add_column("Sessions", justify="right")
            t.add_column("%", justify="right")
            for name, count in sorted(data.items(), key=lambda x: -x[1]):
                t.add_row(name, str(count), f"{count / n * 100:.0f}%")
            console.print(t)
            console.print()

    top = sorted(agg["sessions_per_project"].items(), key=lambda x: -x[1])[:10]
    if top:
        t = Table(title="Top Projects", title_style="bold cyan")
        t.add_column("Project", style="bold")
        t.add_column("Sessions", justify="right")
        for p, c in top:
            t.add_row(p[-50:] if len(p) > 50 else p, str(c))
        console.print(t)
        console.print()


def _clean_project_name(project_path: str) -> str:
    """Derive a clean job name from a project path."""
    path = project_path

    if not path.startswith("/"):
        if "-jobs-" in path:
            path = path[: path.index("-jobs-")]
        path = re.sub(r"^-Users-[^-]+-Downloads-projects-", "", path)
        path = re.sub(r"^-Users-[^-]+-", "", path)
        return path.strip("-") or "unknown"

    boundary = re.search(r"/(runner|jobs)/", path)
    if boundary:
        path = path[: boundary.start()]

    p = Path(path)
    home = Path.home()
    for prefix in [home / "Downloads" / "projects", home]:
        try:
            rel = p.relative_to(prefix)
            name = str(rel).replace("/", "-").replace(" ", "-").strip("-")
            if name and name != ".":
                return name
        except ValueError:
            continue
    return p.name or "unknown"


def _infer_provider(model_name: str | None) -> str:
    """Infer the model provider from the model name."""
    if not model_name:
        return "unknown"
    m = model_name.lower()
    if "claude" in m:
        return "anthropic"
    if "gpt" in m or "o1" in m or "o3" in m or "o4" in m:
        return "openai"
    if "gemini" in m:
        return "google"
    if "llama" in m or "meta" in m:
        return "meta"
    if "deepseek" in m:
        return "deepseek"
    return "unknown"


def main() -> None:
    app()
