"""CLI tool for session-start hook injection.

Reads the last N memories from SQLite, sends them to Gemini for MBEL compression,
and outputs a compact session resume. Also dumps Memory Bank files.

Usage:
    neo-cortex-timeline --n 10
    neo-cortex-timeline --data-dir /path/to/data --mb /path/to/memory_bank
"""

from __future__ import annotations

import argparse
import importlib.resources
import io
import json
import os
import sqlite3
import sys
import time
from pathlib import Path

# Force UTF-8 on stdout/stderr — Windows defaults to CP1250/CP1252 which can't encode MBEL operators
if sys.stdout and sys.stdout.encoding and sys.stdout.encoding.lower().replace("-", "") != "utf8":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if sys.stderr and sys.stderr.encoding and sys.stderr.encoding.lower().replace("-", "") != "utf8":
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")

_debug_enabled = False
_debug_log_path: str | None = None


def _log(msg: str) -> None:
    """Diagnostic log — no-op unless --debug is active."""
    global _debug_log_path
    if not _debug_enabled:
        return
    line = f"[timeline_cli] {msg}"
    print(line, file=sys.stderr)
    if _debug_log_path:
        try:
            with open(_debug_log_path, "a", encoding="utf-8") as f:
                f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {line}\n")
        except OSError as exc:
            print(f"[timeline_cli] WARN: cannot write log: {exc}", file=sys.stderr)
            _debug_log_path = None  # don't retry on every _log call


def _load_mbel_grammar() -> str:
    """Load MBEL grammar from package data."""
    try:
        return importlib.resources.files("neo_cortex").joinpath("mbel.md").read_text(encoding="utf-8")
    except Exception:
        pass
    # Fallback: relative to this file
    pkg_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    mbel_path = os.path.join(pkg_dir, "mbel.md")
    try:
        with open(mbel_path, "r", encoding="utf-8") as f:
            return f.read().strip()
    except FileNotFoundError:
        return "(MBEL grammar not found)"


def _read_mcp_env(var_name: str) -> str | None:
    """Read an env var from .mcp.json neo-cortex server config."""
    for candidate in [
        os.path.join(os.getcwd(), ".mcp.json"),
        os.path.expanduser("~/neo-ram/.mcp.json"),
    ]:
        if os.path.exists(candidate):
            try:
                with open(candidate) as f:
                    data = json.load(f)
                for server in data.get("mcpServers", {}).values():
                    env = server.get("env", {})
                    if var_name in env:
                        return env[var_name]
            except Exception:
                pass
    return None


def _get_gemini_key() -> str | None:
    """Discover Gemini API key from env var or .mcp.json."""
    return os.environ.get("GEMINI_API_KEY") or _read_mcp_env("GEMINI_API_KEY")


def _get_recent_memories(conn: sqlite3.Connection, n: int = 10,
                         project: str | None = None) -> list[dict]:
    """Get last N substantive memories with full content."""
    sql = """
        SELECT title, summary, document, project, activity, topic
        FROM memories
        WHERE summary IS NOT NULL AND length(summary) > 20
          AND activity NOT IN ('discussion')
          AND topic NOT IN ('greeting', 'session start', 'introduction')
    """
    params: list = []
    if project:
        sql += " AND project = ?"
        params.append(project)
    sql += " ORDER BY timestamp DESC LIMIT ?"
    params.append(n)

    rows = conn.execute(sql, params).fetchall()
    return [
        {
            "title": r["title"] or r["topic"] or r["summary"][:80],
            "summary": r["summary"],
            "document": r["document"] or r["summary"],
            "project": r["project"],
            "activity": r["activity"],
        }
        for r in rows
    ]


def _redact_secrets(text: str) -> str:
    """Strip API keys, tokens, and credentials from text before sending to LLM."""
    import re
    patterns = [
        r'AIzaSy[A-Za-z0-9_-]{33}',           # Gemini/Google API keys
        r'gsk_[A-Za-z0-9]{48,}',               # Groq API keys
        r'jina_[A-Za-z0-9]{40,}',              # Jina API keys
        r'sk-[A-Za-z0-9]{32,}',                # OpenAI-style keys
        r'ghp_[A-Za-z0-9]{36}',                # GitHub personal tokens
        r'(?<=[=:"\s])[A-Za-z0-9_-]{32,64}(?=\s|$|")',  # Generic long tokens after = or :
    ]
    for pat in patterns:
        text = re.sub(pat, '[REDACTED]', text)
    return text


def _gemini_resume(memories: list[dict], api_key: str) -> str | None:
    """Call Gemini to compress memories into a MBEL resume."""
    import httpx

    grammar = _load_mbel_grammar()

    # Format memories for Gemini — redact secrets from content
    parts = []
    for i, m in enumerate(memories, 1):
        content = m["document"] or m["summary"]
        if len(content) > 800:
            content = content[:800]
        content = _redact_secrets(content)
        title = _redact_secrets(m["title"])
        parts.append(f"[{i}] ({m['project']}/{m['activity']}) {title}\n{content}")

    memories_text = "\n\n".join(parts)

    prompt = f"""Compress these recent memories into a compact MBEL session resume.
Output ONLY valid MBEL — no markdown code blocks, no explanation.
Max 25 lines. Group by project. Use all MBEL operators.
Include specific details: file paths, versions, function names, numbers.
NEVER include API keys, tokens, passwords, secrets, or credentials in the output. Redact them completely.

{grammar}

EXAMPLE OUTPUT:
§recentWork
[neo-cortex]
✓{{5.10.3}}::published PyPI{{MBEL-native output+compact timeline}}
>distiller::ONE FACT=ONE ENTRY{{was MERGE related turns}}
@threshold::0.65→0.82{{consolidation similarity}}
⚡{{251 memories}}::rebuilt{{was 73→95→199→251}}

Memories to compress:

{memories_text}"""

    try:
        resp = httpx.post(
            f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}",
            json={
                "contents": [{"parts": [{"text": prompt}]}],
                "generationConfig": {"temperature": 0.2, "maxOutputTokens": 1200},
            },
            timeout=12,
        )
        resp.raise_for_status()
        data = resp.json()
        text = data["candidates"][0]["content"]["parts"][0]["text"]
        # Strip markdown code blocks if Gemini wraps them
        text = text.strip()
        if text.startswith("```"):
            lines = text.split("\n")
            lines = [l for l in lines if not l.startswith("```")]
            text = "\n".join(lines).strip()
        return text
    except Exception as e:
        _log(f"Gemini error: {e}")
        return None


def _fallback_titles(memories: list[dict]) -> str:
    """Fallback: just list titles if Gemini is unavailable."""
    lines = []
    for m in memories:
        lines.append(f">{m['activity']}::{m['title']}")
    return "\n".join(lines)


def _get_project_summary(conn: sqlite3.Connection) -> dict[str, dict]:
    """Get per-project stats: count, last activity."""
    rows = conn.execute("""
        SELECT project, COUNT(*) as cnt,
               MAX(timestamp) as last_ts,
               (SELECT activity FROM memories m2
                WHERE m2.project = m1.project ORDER BY timestamp DESC LIMIT 1) as last_activity
        FROM memories m1
        GROUP BY project
        ORDER BY last_ts DESC
    """).fetchall()
    return {
        r["project"]: {"count": r["cnt"], "last_activity": r["last_activity"]}
        for r in rows
    }


def main(argv: list[str] | None = None) -> None:
    parser = argparse.ArgumentParser(description="Cortex session-start context injection")
    parser.add_argument("--data-dir", help="Cortex data directory (has cortex_db/, memory_index.db)")
    parser.add_argument("--n", type=int, default=10, help="Number of recent memories for resume")
    parser.add_argument("--project", default=None, help="Filter by project")
    parser.add_argument("--mb", default=None, help="Memory Bank directory path")
    parser.add_argument("--debug", action="store_true", help="Enable verbose logging to stderr and log file")
    args = parser.parse_args(argv)

    global _debug_enabled, _debug_log_path
    if args.debug:
        _debug_enabled = True

    t0 = time.time()

    # Version
    try:
        from importlib.metadata import version as _pkg_version
        _version = _pkg_version("neo-cortex-mcp")
    except Exception:
        _version = "unknown"

    _log(f"neo-cortex-timeline v{_version}")
    _log(f"started, args={args}")

    # Discover data dir (args → env → .mcp.json → fallback paths)
    data_dir = args.data_dir or os.environ.get("CORTEX_DATA_DIR") or _read_mcp_env("CORTEX_DATA_DIR")
    if not data_dir:
        for candidate in [
            os.path.join(os.getcwd(), ".neo", "cortex", "data"),
            os.path.expanduser("~/neo-ram"),
        ]:
            if os.path.isdir(os.path.join(candidate, "cortex_db")):
                data_dir = candidate
                break

    if not data_dir:
        _log("no data dir found — exiting")
        return

    _log(f"data_dir={data_dir}")

    if args.debug and data_dir:
        _debug_log_path = os.path.join(data_dir, "cortex_db", "session-start.log")
        os.makedirs(os.path.dirname(_debug_log_path), exist_ok=True)
        _log(f"debug log: {_debug_log_path}")
        _log(f"env: CORTEX_DATA_DIR={os.environ.get('CORTEX_DATA_DIR', '<unset>')}")
        _log(f"env: GEMINI_API_KEY={'SET' if os.environ.get('GEMINI_API_KEY') else 'NOT SET'}")
        _log(f"cwd={os.getcwd()}")

    # Discover MB path (memory-bank or memory_bank)
    mb_path = args.mb
    if not mb_path:
        for base in [os.getcwd(), data_dir]:
            for name in ["memory-bank", "memory_bank"]:
                candidate = os.path.join(base, name)
                if os.path.isdir(candidate):
                    mb_path = candidate
                    break
            if mb_path:
                break

    _log(f"mb_path={mb_path}")

    # Open SQLite
    from neo_cortex.subscriber import configure_paths
    configure_paths(data_dir)
    from neo_cortex import config

    db_path = config.MEMORY_INDEX_DB_PATH
    if not Path(db_path).exists():
        _log(f"index db not found at {db_path} — exiting")
        return

    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    total = conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
    sessions_count = conn.execute("SELECT COUNT(DISTINCT session_id) FROM memories").fetchone()[0]
    _log(f"db opened, memories={total}, sessions={sessions_count}")

    # --- Collect output in buffer, then emit + log ---
    out: list[str] = []

    def _emit(line: str = "") -> None:
        out.append(line)

    # --- MBEL grammar ---
    grammar = _load_mbel_grammar()
    if grammar and "(MBEL grammar not found)" not in grammar:
        _emit(grammar)
        _emit()

    # --- Stats ---
    proj_summary = _get_project_summary(conn)
    proj_parts = [f"{p}#{d['count']}" for p, d in list(proj_summary.items())[:5]]
    _emit(f"§cortex @memories::{total} @sessions::{sessions_count}")
    _emit(f"@projects::{'+'.join(proj_parts)}")
    _emit()

    # --- Recent memories → Gemini resume ---
    memories = _get_recent_memories(conn, n=args.n, project=args.project)
    conn.close()

    if memories:
        api_key = _get_gemini_key()
        if api_key:
            _log(f"calling Gemini with {len(memories)} memories")
            resume = _gemini_resume(memories, api_key)
            if resume:
                _emit(resume)
                _emit()
            else:
                _log("Gemini failed, using fallback titles")
                _emit(_fallback_titles(memories))
                _emit()
        else:
            _log("no Gemini API key, using fallback titles")
            _emit(_fallback_titles(memories))
            _emit()

    # --- Memory Bank ---
    if mb_path:
        for fname in ["productContext.md", "systemPatterns.md", "techContext.md", "activeContext.md", "progress.md"]:
            fpath = os.path.join(mb_path, fname)
            if not os.path.exists(fpath):
                continue
            content = Path(fpath).read_text(encoding="utf-8").strip()
            _emit(f"# MB::{fname}")
            _emit(content)
            _emit()

    # --- Flush: stdout for Claude, log for debug ---
    output_text = "\n".join(out)
    print(output_text)

    _log(f"--- INJECTED CONTENT ({len(output_text)} chars) ---")
    _log(output_text)
    _log("--- END INJECTED ---")
    _log(f"done in {time.time() - t0:.2f}s")


if __name__ == "__main__":
    main()
