#!/usr/bin/env python3

import json
import re
import sys
from pathlib import Path

PACKAGE_DIR = Path(__file__).resolve().parent


def sync(project_root: str | None = None):
    root = Path(project_root).resolve() if project_root else Path.cwd().resolve()
    mixer_dir = root / ".mixer"

    sync_settings(root, mixer_dir)
    sync_skill(root, PACKAGE_DIR)
    sync_hooks(root)

    print("Sync complete.")


def sync_skill(root: Path, package_dir: Path):
    """Sync canonical package SKILL.md into project .claude skill path."""
    source_skill = package_dir / "SKILL.md"
    if not source_skill.is_file():
        raise FileNotFoundError(f"Missing canonical skill file: {source_skill}")

    target_skill = root / ".claude" / "skills" / "mixer" / "SKILL.md"
    target_skill.parent.mkdir(parents=True, exist_ok=True)

    target_skill.write_text(source_skill.read_text())
    print(f"  Skill synced: {source_skill} -> {target_skill}")


HOOK_COMMAND = "python3 -m mixersystem.data.conversation_logger"


def sync_hooks(root: Path) -> None:
    """Register Claude Code hooks in .claude/settings.json for conversation logging."""
    settings_path = root / ".claude" / "settings.json"
    settings_path.parent.mkdir(parents=True, exist_ok=True)

    settings: dict = {}
    if settings_path.is_file():
        try:
            loaded = json.loads(settings_path.read_text())
            if isinstance(loaded, dict):
                settings = loaded
        except json.JSONDecodeError:
            settings = {}

    hooks = settings.get("hooks", {})
    if not isinstance(hooks, dict):
        hooks = {}

    def _is_mixer_hook(entry: dict) -> bool:
        """Check if entry is a mixer-owned hook in either old or new format."""
        if not isinstance(entry, dict):
            return False
        # Old format: {"command": HOOK_COMMAND}
        if entry.get("command") == HOOK_COMMAND:
            return True
        # New format: {"hooks": [{"type": "command", "command": HOOK_COMMAND}]}
        inner = entry.get("hooks")
        if isinstance(inner, list):
            return any(
                isinstance(h, dict) and h.get("command") == HOOK_COMMAND
                for h in inner
            )
        return False

    for event in ("UserPromptSubmit", "Stop"):
        entries = hooks.get(event, [])
        if not isinstance(entries, list):
            entries = []

        # Remove any existing mixer-owned entries (old or new format)
        entries = [e for e in entries if not _is_mixer_hook(e)]

        # Add our hook entry in the current Claude Code format
        entries.append({
            "hooks": [{"type": "command", "command": HOOK_COMMAND}]
        })
        hooks[event] = entries

    settings["hooks"] = hooks
    settings_path.write_text(json.dumps(settings, indent=2) + "\n")
    print(f"  Hooks synced: {settings_path}")


_SKIP_DIRS = {
    ".git", ".mixer", ".claude", "node_modules", "__pycache__",
    ".venv", "venv", "dist", "build", ".tox", ".mypy_cache",
}


def _parse_doc_name(doc_path: Path) -> str | None:
    """Extract 'name' from doc.md YAML frontmatter. Returns None if missing."""
    text = doc_path.read_text()
    lines = text.splitlines()
    if not lines or lines[0].strip() != "---":
        return None
    end_index = None
    for i in range(1, len(lines)):
        if lines[i].strip() == "---":
            end_index = i
            break
    if end_index is None:
        return None
    for line in lines[1:end_index]:
        match = re.match(r"^name\s*:\s*(.+)$", line.strip())
        if match:
            value = match.group(1).strip()
            if (value.startswith('"') and value.endswith('"')) or (
                value.startswith("'") and value.endswith("'")
            ):
                value = value[1:-1]
            return value if value else None
    return None


def _scan_doc_tree(directory: Path, root: Path, seen_names: dict[str, Path]) -> dict:
    """Recursively scan a directory for doc.md files, building a hierarchical tree.

    Each node is keyed by the 'name' from doc.md frontmatter. Nodes contain
    a 'doc' path and 'children' dict. Raises on duplicate names.
    """
    result = {}

    doc_file = directory / "doc.md"
    node_name = None
    node = {}
    if doc_file.is_file():
        node_name = _parse_doc_name(doc_file)
        if node_name:
            if node_name in seen_names:
                raise ValueError(
                    f"Duplicate module name '{node_name}' found in "
                    f"{doc_file} and {seen_names[node_name]}"
                )
            seen_names[node_name] = doc_file
            rel = "./" + str(doc_file.relative_to(root))
            node["doc"] = rel

    children = {}
    for child in sorted(directory.iterdir()):
        if not child.is_dir():
            continue
        if child.name.startswith(".") or child.name in _SKIP_DIRS:
            continue
        child_result = _scan_doc_tree(child, root, seen_names)
        children.update(child_result)

    if node_name:
        node["children"] = children
        result[node_name] = node
    else:
        result.update(children)

    return result


def _merge_modules(existing: dict, scanned: dict) -> dict:
    """Merge scanned module tree into existing, preserving user-added keys.

    Scanned entries update ``doc`` and recurse into ``children``, but any
    extra keys the user added to an existing node are kept.  Modules that
    no longer appear in the scan are also kept so user data is never lost.
    """
    merged = dict(existing)
    for name, node in scanned.items():
        if name in merged and isinstance(merged[name], dict):
            old = dict(merged[name])
            old["doc"] = node.get("doc", old.get("doc"))
            old_children = old.get("children", {})
            new_children = node.get("children", {})
            old["children"] = _merge_modules(
                old_children if isinstance(old_children, dict) else {},
                new_children if isinstance(new_children, dict) else {},
            )
            merged[name] = old
        else:
            merged[name] = node
    return merged


def sync_settings(root: Path, mixer_dir: Path):
    """Scan project for doc.md files and generate settings.json."""
    seen_names: dict[str, Path] = {}
    modules = {}
    for child in sorted(root.iterdir()):
        if not child.is_dir():
            continue
        if child.name.startswith(".") or child.name in _SKIP_DIRS:
            continue
        child_result = _scan_doc_tree(child, root, seen_names)
        modules.update(child_result)

    if not modules:
        raise FileNotFoundError(
            f"No modules found in {root}. "
            "Expected at least one doc.md file with a 'name' field in frontmatter."
        )

    mixer_dir.mkdir(parents=True, exist_ok=True)
    settings_path = mixer_dir / "settings.json"
    existing: dict = {}
    if settings_path.is_file():
        try:
            loaded = json.loads(settings_path.read_text())
            if isinstance(loaded, dict):
                existing = loaded
        except json.JSONDecodeError:
            existing = {}

    settings = dict(existing)

    # Merge scanned modules without discarding user-added keys on existing entries.
    existing_modules = settings.get("modules", {})
    if not isinstance(existing_modules, dict):
        existing_modules = {}
    merged_modules = _merge_modules(existing_modules, modules)
    settings["modules"] = merged_modules

    # Preserve the user's linear configuration — only fill in missing defaults.
    linear = settings.get("linear")
    if not isinstance(linear, dict):
        linear = {}
    linear.setdefault("team_prefix", "")
    linear.setdefault("team_id", "")
    settings["linear"] = linear

    content = json.dumps(settings, indent=2) + "\n"
    settings_path.write_text(content)

    names = sorted(seen_names.keys())
    print(f"  Settings generated (modules: {', '.join(names)})")


if __name__ == "__main__":
    project_root = None
    for arg in sys.argv[1:]:
        if arg.startswith("--project_root="):
            project_root = arg.split("=", 1)[1]
    sync(project_root=project_root)
