#!/usr/bin/env python3
"""PreCompact hook: save conversation observations to claude-mem before compaction.

Reads the session transcript, extracts key observations (decisions, bugs,
features, patterns), and writes them directly to claude-mem's SQLite database.

Input (stdin JSON from Claude Code):
{
  "session_id": "...",
  "transcript_path": "/path/to/session.jsonl",
  "hook_event_name": "PreCompact",
  "trigger": "auto" | "manual",
  ...
}
"""

import hashlib
import json
import os
import sqlite3
import sys
import time
from datetime import UTC, datetime
from pathlib import Path

DB_PATH = Path.home() / ".claude-mem" / "claude-mem.db"
MAX_TRANSCRIPT_MB = 50


def get_project_from_cwd() -> str:
    """Derive project name from current working directory."""
    cwd = os.getcwd()
    # Try to find project name from path
    parts = Path(cwd).parts
    for i, part in enumerate(parts):
        if part in ("projects", "repos", "src") and i + 1 < len(parts):
            return parts[i + 1]
    return Path(cwd).name


def read_transcript(path: str) -> list[dict]:
    """Read JSONL transcript file, return list of message dicts."""
    messages = []
    transcript = Path(path)
    if not transcript.exists():
        return messages

    # Safety: skip huge transcripts
    if transcript.stat().st_size > MAX_TRANSCRIPT_MB * 1024 * 1024:
        return messages

    with open(transcript) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                messages.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return messages


def extract_observations(messages: list[dict]) -> list[dict]:
    """Extract key observations from transcript messages.

    Claude Code transcript format: each line is a JSON object with:
    - type: "assistant" | "user" | "system" | "progress" | ...
    - message.role: "assistant" | "user"
    - message.content: list of {type: "text", text: "..."} or {type: "tool_use", ...}

    Looks for patterns that indicate important moments:
    - Decisions made (architecture, technology choices)
    - Bugs found and fixed
    - Features implemented
    - Files created or modified
    - Key discoveries about the codebase
    """
    observations = []
    files_modified = set()
    files_read = set()

    for msg in messages:
        msg_type = msg.get("type", "")
        if msg_type not in ("assistant", "user"):
            continue

        # Extract from the nested message structure
        inner = msg.get("message", {})
        if not inner:
            continue

        role = inner.get("role", msg_type)
        content = inner.get("content", "")

        # Handle content that's a list of blocks
        if isinstance(content, list):
            text_parts = []
            for block in content:
                if isinstance(block, dict):
                    if block.get("type") == "text":
                        text_parts.append(block.get("text", ""))
                    elif block.get("type") == "tool_use":
                        tool_name = block.get("name", "")
                        tool_input = block.get("input", {})
                        _track_tool_use(tool_name, tool_input, files_modified, files_read)
                elif isinstance(block, str):
                    text_parts.append(block)
            content = "\n".join(text_parts)

        if not isinstance(content, str) or not content.strip():
            continue

        # Extract observations from assistant messages
        if role == "assistant":
            _extract_from_assistant(content, observations)

        # Extract from user messages (requirements, corrections)
        elif role == "user":
            _extract_from_user(content, observations)

    # Add file tracking observation
    if files_modified:
        observations.append(
            {
                "type": "change",
                "title": f"Files modified during session ({len(files_modified)} files)",
                "text": "Modified: " + ", ".join(sorted(files_modified)[:30]),
            }
        )

    return observations


def _track_tool_use(
    tool_name: str,
    tool_input: dict,
    files_modified: set,
    files_read: set,
) -> None:
    """Track file operations from tool use blocks."""
    if tool_name in ("Edit", "Write", "NotebookEdit"):
        path = tool_input.get("file_path", "")
        if path:
            files_modified.add(Path(path).name)
    elif tool_name == "Read":
        path = tool_input.get("file_path", "")
        if path:
            files_read.add(Path(path).name)


def _extract_from_assistant(content: str, observations: list[dict]) -> None:
    """Extract observations from assistant messages."""
    lines = content.split("\n")

    for line in lines:
        line_stripped = line.strip()
        lower = line_stripped.lower()

        # Decision patterns
        if any(kw in lower for kw in ["decided to", "the approach", "chose to", "going with"]):
            if len(line_stripped) > 20:
                observations.append(
                    {
                        "type": "decision",
                        "title": _truncate(line_stripped, 80),
                        "text": line_stripped,
                    }
                )

        # Bug fix patterns
        elif any(kw in lower for kw in ["fixed", "the bug", "root cause", "the issue was"]):
            if len(line_stripped) > 15:
                observations.append(
                    {
                        "type": "discovery",
                        "title": _truncate(line_stripped, 80),
                        "text": line_stripped,
                    }
                )

        # Implementation patterns
        elif (
            any(kw in lower for kw in ["implemented", "created", "added new"])
            and len(line_stripped) > 20
        ):
            observations.append(
                {
                    "type": "change",
                    "title": _truncate(line_stripped, 80),
                    "text": line_stripped,
                }
            )


def _extract_from_user(content: str, observations: list[dict]) -> None:
    """Extract observations from user messages (corrections, requirements)."""
    lower = content.lower().strip()

    # User corrections are valuable signals
    correction_kw = [
        "\u043d\u0435 \u0442\u0430\u043a",
        "wrong",
        "fix",
        "\u043f\u043e\u043c\u0435\u043d\u044f\u0439",
        "\u0438\u0441\u043f\u0440\u0430\u0432\u044c",
    ]
    if any(kw in lower for kw in correction_kw) and 10 < len(content) < 500:
        observations.append(
            {
                "type": "discovery",
                "title": "User correction: " + _truncate(content, 60),
                "text": content[:500],
            }
        )


def _truncate(text: str, max_len: int) -> str:
    """Truncate text to max_len, adding ellipsis."""
    # Remove markdown formatting for title
    clean = text.lstrip("#- *>").strip()
    if len(clean) > max_len:
        return clean[: max_len - 3] + "..."
    return clean


def deduplicate(observations: list[dict]) -> list[dict]:
    """Remove duplicate observations by content hash."""
    seen = set()
    unique = []
    for obs in observations:
        h = hashlib.md5(obs["text"].encode()).hexdigest()  # noqa: S324
        if h not in seen:
            seen.add(h)
            unique.append(obs)
    return unique


def save_to_db(observations: list[dict], session_id: str, project: str) -> int:
    """Insert observations directly into claude-mem SQLite database."""
    if not DB_PATH.exists():
        return 0

    now = datetime.now(UTC)
    now_iso = now.isoformat()
    now_epoch = int(now.timestamp() * 1000)

    conn = sqlite3.connect(str(DB_PATH))
    saved = 0

    try:
        # Ensure session exists
        conn.execute(
            """INSERT OR IGNORE INTO sdk_sessions
               (content_session_id, memory_session_id, project,
                started_at, started_at_epoch, status)
               VALUES (?, ?, ?, ?, ?, 'active')""",
            (session_id, f"precompact-{session_id}", project, now_iso, now_epoch),
        )

        for obs in observations:
            content_hash = hashlib.md5(obs["text"].encode()).hexdigest()  # noqa: S324

            # Skip if already exists (by content hash in last hour)
            cursor = conn.execute(
                """SELECT id FROM observations
                   WHERE content_hash = ? AND created_at_epoch > ?""",
                (content_hash, now_epoch - 3600000),
            )
            if cursor.fetchone():
                continue

            conn.execute(
                """INSERT INTO observations
                   (memory_session_id, project, text, type, title, content_hash,
                    created_at, created_at_epoch)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    f"precompact-{session_id}",
                    project,
                    obs["text"],
                    obs.get("type", "discovery"),
                    obs.get("title", ""),
                    content_hash,
                    now_iso,
                    now_epoch,
                ),
            )
            saved += 1

        conn.commit()
    finally:
        conn.close()

    return saved


def main() -> None:
    """Main hook entry point. Reads stdin JSON, extracts observations, saves to DB."""
    try:
        raw = sys.stdin.read()
        if not raw.strip():
            return

        hook_input = json.loads(raw)
    except (json.JSONDecodeError, OSError):
        return

    transcript_path = hook_input.get("transcript_path", "")
    session_id = hook_input.get("session_id", "precompact-" + str(int(time.time())))
    project = get_project_from_cwd()

    if not transcript_path:
        return

    messages = read_transcript(transcript_path)
    if not messages:
        return

    observations = extract_observations(messages)
    observations = deduplicate(observations)

    if not observations:
        return

    # Cap at 50 observations per compaction
    observations = observations[:50]

    saved = save_to_db(observations, session_id, project)

    # Output to stdout (becomes hook result in Claude Code)
    if saved > 0:
        print(f"PreCompact: saved {saved} observations to claude-mem")


if __name__ == "__main__":
    main()
