"""loci CLI — command-line interface for LociDB."""

from __future__ import annotations

import json
import sys
from pathlib import Path

import click

from ..core.db import LociDB
from ..query.executor import execute


DEFAULT_DB = "./memory.loci"


def _get_db(path: str) -> LociDB:
    return LociDB(path=path)


@click.group()
@click.option("--db", default=DEFAULT_DB, envvar="LOCI_DB", help="Path to the .loci database file.")
@click.pass_context
def cli(ctx: click.Context, db: str) -> None:
    """LociDB — cognitive database for LLM memory."""
    ctx.ensure_object(dict)
    ctx.obj["db_path"] = db


@cli.command()
@click.pass_context
def init(ctx: click.Context) -> None:
    """Create a new .loci database."""
    db_path = ctx.obj["db_path"]
    if Path(db_path).exists():
        click.echo(f"Database already exists at {db_path}")
        return
    db = _get_db(db_path)
    db.close()
    click.echo(f"Initialized new LociDB at {db_path}")


@cli.command()
@click.argument("text")
@click.option("--entities", "-e", multiple=True, help="Entity tags.")
@click.option("--scope", "-s", default="global", help="Context scope.")
@click.pass_context
def memorize(ctx: click.Context, text: str, entities: tuple, scope: str) -> None:
    """Store a new memory."""
    db = _get_db(ctx.obj["db_path"])
    try:
        node = db.memorize(content=text, entities=list(entities), scope=scope)
        click.echo(f"Memorized [{node.id[:8]}] importance={node.importance:.3f}")
    finally:
        db.close()


@cli.command()
@click.argument("query_text")
@click.option("--top-k", "-k", default=5, help="Number of results.")
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.option("--entities", "-e", multiple=True, help="Entity filter.")
@click.option("--days", "-d", default=None, type=int, help="Time window in days.")
@click.option("--min-importance", "-m", default=0.0, type=float, help="Minimum importance.")
@click.pass_context
def recall(
    ctx: click.Context,
    query_text: str,
    top_k: int,
    scope: str | None,
    entities: tuple,
    days: int | None,
    min_importance: float,
) -> None:
    """Search memories by semantic similarity."""
    db = _get_db(ctx.obj["db_path"])
    try:
        results = db.recall(
            query=query_text,
            entities=list(entities) or None,
            scope=scope,
            time_window_days=days,
            min_importance=min_importance,
            top_k=top_k,
        )
        if not results:
            click.echo("No memories found.")
            return
        for i, r in enumerate(results, 1):
            node = r["node"]
            click.echo(
                f"{i}. [{r['composite_score']:.3f}] {node.content}\n"
                f"   entities={node.entity_tags}  scope={node.context_scope}  "
                f"importance={r['importance']:.3f}  similarity={r['similarity']:.3f}"
            )
    finally:
        db.close()


@cli.command()
@click.pass_context
def inspect(ctx: click.Context) -> None:
    """Show database statistics."""
    db_path = ctx.obj["db_path"]
    if not Path(db_path).exists():
        click.echo(f"No database found at {db_path}. Run `loci init` first.")
        return
    db = _get_db(db_path)
    try:
        s = db.stats()
        click.echo(f"LociDB: {db_path}")
        click.echo(f"  Nodes:  {s['node_count']}")
        click.echo(f"  Edges:  {s['edge_count']}")
        click.echo(f"  Scopes: {', '.join(s['scopes']) if s['scopes'] else '(none)'}")
        click.echo(f"  Avg importance: {s['avg_importance']:.4f}")
    finally:
        db.close()


@cli.command("query")
@click.argument("mql")
@click.pass_context
def run_query(ctx: click.Context, mql: str) -> None:
    """Execute a raw MQL query."""
    db = _get_db(ctx.obj["db_path"])
    try:
        results = execute(db, mql)
        for r in results:
            click.echo(json.dumps(r, indent=2, default=str))
    finally:
        db.close()


@cli.command()
@click.option("--scope", "-s", default=None, help="Scope to compress.")
@click.option("--older-than", default=2, type=int, help="Compress memories older than N days.")
@click.pass_context
def compress(ctx: click.Context, scope: str | None, older_than: int) -> None:
    """Compress old memories into daily summaries."""
    db = _get_db(ctx.obj["db_path"])
    try:
        count = db.compress(scope=scope, older_than_days=older_than)
        click.echo(f"Created {count} summary node(s).")
    finally:
        db.close()


@cli.command()
@click.argument("node_id")
@click.pass_context
def forget(ctx: click.Context, node_id: str) -> None:
    """Delete a memory by node ID."""
    db = _get_db(ctx.obj["db_path"])
    try:
        db.forget(node_id)
        click.echo(f"Forgotten node {node_id}")
    finally:
        db.close()


@cli.command("export")
@click.option("--format", "-f", "fmt", default="native",
              type=click.Choice(["native", "context_prompt", "snapshot"]),
              help="Export format.")
@click.option("--output", "-o", default=None, help="Output file path.")
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.option("--max-tokens", "-t", default=4000, type=int, help="Token budget (context_prompt/snapshot).")
@click.option("--min-importance", "-m", default=0.0, type=float, help="Minimum importance.")
@click.option("--days", "-d", default=None, type=int, help="Time window in days.")
@click.pass_context
def export_cmd(
    ctx: click.Context,
    fmt: str,
    output: str | None,
    scope: str | None,
    max_tokens: int,
    min_importance: float,
    days: int | None,
) -> None:
    """Export memories to a file."""
    if output is None:
        ext = {"native": ".loci.json", "context_prompt": ".context.md", "snapshot": ".snapshot.txt"}
        output = f"memory{ext.get(fmt, '.json')}"

    db = _get_db(ctx.obj["db_path"])
    try:
        result = db.export(
            path=output,
            format=fmt,
            max_tokens=max_tokens,
            scope=scope,
            min_importance=min_importance,
            time_window_days=days,
        )
        click.echo(f"Exported to {result}")
    finally:
        db.close()


@cli.command("import")
@click.argument("filepath")
@click.option("--mode", default="replace", type=click.Choice(["replace", "merge"]),
              help="Import mode: replace existing or merge.")
@click.pass_context
def import_cmd(ctx: click.Context, filepath: str, mode: str) -> None:
    """Import memories from a file."""
    if not Path(filepath).exists():
        click.echo(f"File not found: {filepath}")
        return
    db = _get_db(ctx.obj["db_path"])
    try:
        result = db.import_from(path=filepath, mode=mode)
        click.echo(f"Imported {result.get('imported_nodes', 0)} node(s) (mode={mode})")
    finally:
        db.close()


# ------------------------------------------------------------------
# Persona commands
# ------------------------------------------------------------------

@cli.group()
def persona() -> None:
    """Manage user persona."""
    pass


@persona.command("set")
@click.argument("key")
@click.argument("value")
@click.pass_context
def persona_set(ctx: click.Context, key: str, value: str) -> None:
    """Set a persona attribute."""
    db = _get_db(ctx.obj["db_path"])
    try:
        db.set_persona(key, value)
        click.echo(f"Set persona.{key} = {value}")
    finally:
        db.close()


@persona.command("show")
@click.pass_context
def persona_show(ctx: click.Context) -> None:
    """Show all persona attributes."""
    db = _get_db(ctx.obj["db_path"])
    try:
        data = db.get_persona()
        if not data:
            click.echo("No persona configured.")
            return
        for k, v in sorted(data.items()):
            click.echo(f"  {k}: {v}")
    finally:
        db.close()


@persona.command("clear")
@click.pass_context
def persona_clear(ctx: click.Context) -> None:
    """Clear all persona data."""
    db = _get_db(ctx.obj["db_path"])
    try:
        db.clear_persona()
        click.echo("Persona cleared.")
    finally:
        db.close()


# ------------------------------------------------------------------
# Audit trail commands
# ------------------------------------------------------------------

@cli.command()
@click.argument("trace_id")
@click.pass_context
def explain(ctx: click.Context, trace_id: str) -> None:
    """Explain how a response was formed (by trace ID)."""
    db = _get_db(ctx.obj["db_path"])
    try:
        text = db.explain(trace_id)
        if text:
            click.echo(text)
        else:
            click.echo(f"Trace {trace_id} not found.")
    finally:
        db.close()


@cli.command()
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.option("--limit", "-l", default=10, type=int, help="Max traces to show.")
@click.pass_context
def traces(ctx: click.Context, scope: str | None, limit: int) -> None:
    """List recent reasoning traces."""
    db = _get_db(ctx.obj["db_path"])
    try:
        results = db.traces(scope=scope, limit=limit)
        if not results:
            click.echo("No traces found.")
            return
        for t in results:
            ts = t["timestamp"].strftime("%Y-%m-%d %H:%M")
            click.echo(f"  [{t['id'][:8]}] {ts} — {t['query'][:60]}")
    finally:
        db.close()


# ------------------------------------------------------------------
# Health command
# ------------------------------------------------------------------

@cli.command()
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.pass_context
def health(ctx: click.Context, scope: str | None) -> None:
    """Show memory health score and diagnostics."""
    db = _get_db(ctx.obj["db_path"])
    try:
        report = db.health(scope=scope)
        click.echo(f"Overall Health: {report.overall_score}/100")
        click.echo("Dimensions:")
        for dim, score in sorted(report.dimensions.items()):
            bar = "█" * int(score / 10) + "░" * (10 - int(score / 10))
            click.echo(f"  {dim:15s} {bar} {score:.0f}")
        if report.gaps:
            click.echo("Gaps:")
            for g in report.gaps:
                click.echo(f"  ⚠ {g}")
        if report.suggestions:
            click.echo("Suggestions:")
            for s in report.suggestions:
                click.echo(f"  → {s}")
    finally:
        db.close()


# ------------------------------------------------------------------
# Time-travel commands
# ------------------------------------------------------------------

@cli.command()
@click.argument("date")
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.pass_context
def snapshot(ctx: click.Context, date: str, scope: str | None) -> None:
    """Show memories as they existed at a date (YYYY-MM-DD)."""
    from datetime import datetime, timezone
    try:
        as_of = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc, hour=23, minute=59, second=59)
    except ValueError:
        click.echo("Invalid date format. Use YYYY-MM-DD.")
        return
    db = _get_db(ctx.obj["db_path"])
    try:
        nodes = db.snapshot(as_of=as_of, scope=scope)
        click.echo(f"Snapshot at {date}: {len(nodes)} memories")
        for n in nodes[:20]:
            click.echo(f"  [{n.id[:8]}] {n.content[:80]}")
    finally:
        db.close()


@cli.command()
@click.option("--entity", "-e", default=None, help="Filter by entity.")
@click.option("--scope", "-s", default=None, help="Filter by scope.")
@click.option("--limit", "-l", default=20, type=int, help="Max entries.")
@click.pass_context
def timeline(ctx: click.Context, entity: str | None, scope: str | None, limit: int) -> None:
    """Show memories in chronological order."""
    db = _get_db(ctx.obj["db_path"])
    try:
        nodes = db.timeline(entity=entity, scope=scope, limit=limit)
        for n in nodes:
            ts = n.timestamp.strftime("%Y-%m-%d %H:%M")
            status = " [deleted]" if n.deleted_at else ""
            click.echo(f"  {ts} [{n.id[:8]}]{status} {n.content[:70]}")
    finally:
        db.close()


if __name__ == "__main__":
    cli()
