"""Kioku Lite CLI — zero Docker, zero cloud LLM.

Commands:
  save        Save a memory. Returns content_hash for kg-index.
  search      Tri-hybrid search (BM25 + Vector + Graph).
  kg-index    Agent-provided entity/relationship indexing for a saved memory.
  kg-alias    Register SAME_AS aliases for a canonical entity.
  recall      Recall everything related to an entity.
  connect     Explain connection between two entities.
  entities    List top entities in the knowledge graph.
  timeline    Chronological memory list.
  setup       First-time setup (download embedding model, create config).
  init        Generate CLAUDE.md + SKILL.md for Claude Code / Cursor.
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from typing import Optional

try:
    import typer
except ImportError:
    raise ImportError("Install with: pip install kioku-agent-kit-lite[cli]")

app = typer.Typer(
    name="kioku-lite",
    help="Personal memory agent — zero Docker, zero cloud LLM.",
    no_args_is_help=True,
)

_svc = None


def _get_svc():
    global _svc
    if _svc is None:
        from kioku_lite.service import KiokuLiteService
        _svc = KiokuLiteService()
    return _svc


def _out(data: dict | str) -> None:
    if isinstance(data, str):
        typer.echo(data)
    else:
        typer.echo(json.dumps(data, ensure_ascii=False, indent=2))


# ── save ───────────────────────────────────────────────────────────────────────

@app.command()
def save(
    text: str = typer.Argument(..., help="Memory text to save."),
    mood: Optional[str] = typer.Option(None, "--mood", "-m"),
    tags: Optional[str] = typer.Option(None, "--tags", "-t", help="Comma-separated tags."),
    event_time: Optional[str] = typer.Option(None, "--event-time", "-e", help="When it happened (YYYY-MM-DD)."),
) -> None:
    """Save a memory. Prints content_hash — use it with kg-index to add KG entries."""
    tag_list = [t.strip() for t in tags.split(",")] if tags else None
    result = _get_svc().save_memory(text, mood=mood, tags=tag_list, event_time=event_time)
    _out(result)


# ── kg-index ───────────────────────────────────────────────────────────────────

@app.command(name="kg-index")
def kg_index(
    content_hash: str = typer.Argument(..., help="content_hash returned by `save`."),
    entities: Optional[str] = typer.Option(
        None, "--entities", "-e",
        help='JSON array of entities. Example: \'[{"name":"Phúc","type":"PERSON"}]\''
    ),
    relationships: Optional[str] = typer.Option(
        None, "--relationships", "-r",
        help='JSON array of relationships. Example: \'[{"source":"Phúc","target":"TBV","rel_type":"WORKS_AT","weight":0.8,"evidence":"..."}]\''
    ),
    event_time: Optional[str] = typer.Option(None, "--event-time", help="YYYY-MM-DD when the event happened."),
) -> None:
    """Index agent-extracted entities and relationships for a saved memory.

    The agent (Claude Code / OpenClaw) calls this AFTER `save`, providing
    entities and relationships it already extracted from conversation context.
    Kioku Lite does NOT call any LLM — it simply stores what the agent provides.

    Example workflow (agent SKILL.md):

      1. hash = `kioku-lite save "Hôm nay gặp Hùng ở TBV..." --mood happy`
      2. Agent extracts: entities=[Hùng/PERSON, TBV/PLACE], rel=[Hùng→TBV WORKS_AT]
      3. `kioku-lite kg-index <hash> --entities '[...]' --relationships '[...]'`
    """
    from kioku_lite.service import EntityInput, RelationshipInput

    entity_list: list[EntityInput] = []
    if entities:
        try:
            raw = json.loads(entities)
            entity_list = [EntityInput(name=e["name"], type=e.get("type", "TOPIC")) for e in raw]
        except (json.JSONDecodeError, KeyError) as err:
            typer.echo(f"Error parsing --entities JSON: {err}", err=True)
            raise typer.Exit(1)

    rel_list: list[RelationshipInput] = []
    if relationships:
        try:
            raw = json.loads(relationships)
            rel_list = [
                RelationshipInput(
                    source=r["source"], target=r["target"],
                    rel_type=r.get("rel_type", "TOPICAL"),
                    weight=r.get("weight", 0.5),
                    evidence=r.get("evidence", ""),
                )
                for r in raw
            ]
        except (json.JSONDecodeError, KeyError) as err:
            typer.echo(f"Error parsing --relationships JSON: {err}", err=True)
            raise typer.Exit(1)

    result = _get_svc().kg_index(content_hash, entity_list, rel_list, event_time=event_time)
    _out(result)


# ── kg-alias ───────────────────────────────────────────────────────────────────

@app.command(name="kg-alias")
def kg_alias(
    canonical: str = typer.Argument(..., help="The canonical entity name."),
    aliases: str = typer.Option(
        ..., "--aliases", "-a",
        help='JSON array of alias names. Example: \'["phuc-nt","anh","Phúc"]\''
    ),
) -> None:
    """Register SAME_AS aliases for a canonical entity.

    Example:
      kioku-lite kg-alias "Nguyễn Trọng Phúc" --aliases '["phuc-nt","Phúc","anh","tôi"]'
    """
    try:
        alias_list = json.loads(aliases)
    except json.JSONDecodeError as err:
        typer.echo(f"Error parsing --aliases JSON: {err}", err=True)
        raise typer.Exit(1)
    _out(_get_svc().kg_alias(canonical, alias_list))


# ── search ─────────────────────────────────────────────────────────────────────

@app.command()
def search(
    query: str = typer.Argument(..., help="What to search for."),
    limit: int = typer.Option(10, "--limit", "-l"),
    date_from: Optional[str] = typer.Option(None, "--from", help="Start date YYYY-MM-DD."),
    date_to: Optional[str] = typer.Option(None, "--to", help="End date YYYY-MM-DD."),
    entities: Optional[str] = typer.Option(None, "--entities", "-e", help="Comma-separated entity names for KG seeding."),
) -> None:
    """Tri-hybrid search: BM25 + Vector (FastEmbed) + Knowledge Graph → RRF rerank."""
    entity_list = [e.strip() for e in entities.split(",")] if entities else None
    _out(_get_svc().search_memories(query, limit=limit, date_from=date_from, date_to=date_to, entities=entity_list))


# ── recall ─────────────────────────────────────────────────────────────────────

@app.command()
def recall(
    entity: str = typer.Argument(..., help="Entity name to recall memories for."),
    hops: int = typer.Option(2, "--hops", help="Graph traversal depth."),
    limit: int = typer.Option(10, "--limit", "-l"),
) -> None:
    """Recall all memories related to an entity via knowledge graph traversal."""
    _out(_get_svc().recall_entity(entity, max_hops=hops, limit=limit))


# ── connect ────────────────────────────────────────────────────────────────────

@app.command()
def connect(
    entity_a: str = typer.Argument(...),
    entity_b: str = typer.Argument(...),
) -> None:
    """Explain how two entities are connected in the knowledge graph."""
    _out(_get_svc().explain_connection(entity_a, entity_b))


# ── entities ───────────────────────────────────────────────────────────────────

@app.command()
def entities(
    limit: int = typer.Option(50, "--limit", "-l"),
) -> None:
    """List top canonical entities from the knowledge graph."""
    _out(_get_svc().list_entities(limit=limit))


# ── timeline ───────────────────────────────────────────────────────────────────

@app.command()
def timeline(
    start_date: Optional[str] = typer.Option(None, "--from"),
    end_date: Optional[str] = typer.Option(None, "--to"),
    limit: int = typer.Option(50, "--limit", "-l"),
    sort_by: str = typer.Option("processing_time", "--sort-by", "-s", help="'processing_time' or 'event_time'."),
) -> None:
    """Chronological memory list."""
    _out(_get_svc().get_timeline(start_date=start_date, end_date=end_date, limit=limit, sort_by=sort_by))


# ── users ──────────────────────────────────────────────────────────────────────

@app.command()
def users(
    create: Optional[str] = typer.Option(None, "--create", "-c", help="Create a new profile."),
    use: Optional[str] = typer.Option(None, "--use", "-u", help="Set active profile for this session."),
) -> None:
    """List all user profiles, create a new one, or switch active profile.

    Run at the start of each session to pick which memory profile to use.
    After --use, all subsequent kioku-lite commands use that profile automatically.

    \b
    # List profiles:
    kioku-lite users

    # Switch to a profile (no prefix needed after this):
    kioku-lite users --use work

    # Create a new profile:
    kioku-lite users --create work
    """
    base_dir = Path.home() / ".kioku-lite" / "users"
    active_file = Path.home() / ".kioku-lite" / ".active_user"

    # ── --use: set active session profile ────────────────────────────────────
    if use:
        # Profile must exist
        if not (base_dir / use).exists() and use != "personal":
            typer.echo(f"⚠️  Profile '{use}' not found. Create it first: kioku-lite users --create {use}", err=True)
            raise typer.Exit(1)
        # Ensure dir exists for personal
        (base_dir / use / "data").mkdir(parents=True, exist_ok=True)
        (base_dir / use / "memory").mkdir(parents=True, exist_ok=True)
        # Write active profile file
        active_file.parent.mkdir(parents=True, exist_ok=True)
        active_file.write_text(use)
        _out({"status": "active", "user_id": use, "note": "All subsequent kioku-lite commands will use this profile."})
        return

    # ── --create: make new profile ────────────────────────────────────────────
    if create:
        if not create.replace("-", "").replace("_", "").isalnum():
            typer.echo("⚠️  Profile ID can only contain letters, numbers, hyphens and underscores.", err=True)
            raise typer.Exit(1)
        profile_dir = base_dir / create / "data"
        profile_dir.mkdir(parents=True, exist_ok=True)
        (base_dir / create / "memory").mkdir(parents=True, exist_ok=True)
        _out({"status": "created", "user_id": create, "path": str(base_dir / create)})
        typer.echo(f"\nActivate it now: kioku-lite users --use {create}")
        return

    # ── list: show all profiles ───────────────────────────────────────────────
    # Ensure default profile "personal" always exists
    (base_dir / "personal" / "data").mkdir(parents=True, exist_ok=True)
    (base_dir / "personal" / "memory").mkdir(parents=True, exist_ok=True)

    # Read current active profile
    current_active = active_file.read_text().strip() if active_file.exists() else "personal"

    profiles = []
    if base_dir.exists():
        for p in sorted(base_dir.iterdir()):
            if p.is_dir():
                db = p / "data" / "kioku.db"
                profiles.append({
                    "user_id": p.name,
                    "active": p.name == current_active,
                    "has_data": db.exists(),
                    "db_size_kb": round(db.stat().st_size / 1024, 1) if db.exists() else 0,
                })

    _out({
        "profiles": profiles,
        "active_profile": current_active,
        "hint": "Run 'kioku-lite users --use <user_id>' to switch profiles",
    })

# ── setup ──────────────────────────────────────────────────────────────────────

@app.command()
def setup() -> None:
    """Pre-download the embedding model (~1.1GB, first-time only).

    Optional — kioku-lite works without running this. The model downloads
    automatically on first use. Run this to eagerly pre-download before
    going offline, or to verify the local install.

    \b
    Profile management: kioku-lite users
    Agent integration:  kioku-lite init --global
    """
    embed_model = "intfloat/multilingual-e5-large"

    typer.echo("")
    typer.echo("╔══════════════════════════════════════╗")
    typer.echo("║   Kioku Lite — Pre-download Model    ║")
    typer.echo("╚══════════════════════════════════════╝")
    typer.echo(f"\nModel  : {embed_model}")
    typer.echo("Target : ~/.cache/fastembed/")
    typer.echo("Size   : ~1.1GB (once only)\n")

    try:
        from kioku_lite.pipeline.embedder import FastEmbedder
        embedder = FastEmbedder(model_name=embed_model)
        embedder.embed("warmup")
        typer.echo("\n✅ Model ready.")
    except Exception as e:
        typer.echo(f"\n⚠️  Download failed: {e}")
        typer.echo("   Run again when online.")
        typer.echo("   Or: KIOKU_LITE_EMBED_PROVIDER=fake kioku-lite save '...'  (BM25+Graph only, no vectors)")

    typer.echo("\nNext steps:")
    typer.echo("  kioku-lite users --use personal   # pick active profile")
    typer.echo("  kioku-lite init --global           # inject SKILL.md for Claude Code")
    typer.echo('  kioku-lite save "Your first memory"')
    typer.echo("")





# ── init ───────────────────────────────────────────────────────────────────────

@app.command()
def init(
    global_: bool = typer.Option(
        False, "--global", "-g",
        help="Install globally into ~/.claude/ — works in ALL projects without re-running init.",
    ),
) -> None:
    """Generate CLAUDE.md + SKILL.md for Claude Code / Cursor agent integration.

    By default: writes to the current project directory.

    With --global: installs SKILL.md into ~/.claude/skills/kioku-lite/SKILL.md
    so Claude Code picks it up in EVERY project automatically. Recommended.

    \b
    # One-time global setup (recommended):
    kioku-lite init --global

    # Per-project setup (if you want project-specific):
    kioku-lite init
    """
    RESOURCES = Path(__file__).parent / "resources"

    claude_src = RESOURCES / "CLAUDE.agent.md"
    skill_src = RESOURCES / "SKILL.md"

    if not claude_src.exists() or not skill_src.exists():
        typer.echo("⚠️  Resource files not found. Reinstall: pip install 'kioku-lite[cli]'", err=True)
        raise typer.Exit(1)

    if global_:
        # Write to ~/.claude/ — Claude Code reads this from any project
        skill_dir = Path.home() / ".claude" / "skills" / "kioku-lite"
        skill_dir.mkdir(parents=True, exist_ok=True)
        skill_dst = skill_dir / "SKILL.md"
        skill_dst.write_text(skill_src.read_text(encoding="utf-8"))

        typer.echo("")
        typer.echo("✅ Global install:")
        typer.echo(f"   {skill_dst}")
        typer.echo("")
        typer.echo("Claude Code will now use kioku-lite in EVERY project automatically.")
        typer.echo("No need to run `kioku-lite init` per project.")
        typer.echo("")
        typer.echo("Note: No CLAUDE.md is written globally — Claude reads SKILL.md via")
        typer.echo("      the skills directory. To also add a CLAUDE.md to a specific")
        typer.echo("      project, run `kioku-lite init` (without --global) there.")
        typer.echo("")
    else:
        # Write to current project directory
        claude_dst = Path.cwd() / "CLAUDE.md"
        skill_dir = Path.cwd() / ".claude" / "skills" / "kioku-lite"
        skill_dir.mkdir(parents=True, exist_ok=True)
        skill_dst = skill_dir / "SKILL.md"

        claude_dst.write_text(claude_src.read_text(encoding="utf-8"))
        skill_dst.write_text(skill_src.read_text(encoding="utf-8"))

        typer.echo("")
        typer.echo("✅ Project install:")
        typer.echo(f"   {claude_dst}")
        typer.echo(f"   {skill_dst}")
        typer.echo("")
        typer.echo("Claude Code will use kioku-lite in THIS project.")
        typer.echo("")
        typer.echo("Tip: Run `kioku-lite init --global` once to enable in ALL projects.")
        typer.echo("")



if __name__ == "__main__":
    app()
