#!/usr/bin/env python3

import json
import re
import sys
from pathlib import Path

PACKAGE_DIR = Path(__file__).resolve().parent
SYNC_GUIDE_START = "<!-- SKILL_SYNC_WORKFLOW_GUIDE_START -->"
SYNC_GUIDE_END = "<!-- SKILL_SYNC_WORKFLOW_GUIDE_END -->"
SKILL_MANAGED_START = "<!-- MIXER_SYNC_WORKFLOW_GUIDE_START -->"
SKILL_MANAGED_END = "<!-- MIXER_SYNC_WORKFLOW_GUIDE_END -->"


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)
    _write_runtime_notice(mixer_dir, PACKAGE_DIR)

    print("Sync complete.")


def _write_runtime_notice(mixer_dir: Path, package_dir: Path) -> None:
    mixer_dir.mkdir(parents=True, exist_ok=True)
    source_readme = package_dir / "README.md"
    if not source_readme.is_file():
        raise FileNotFoundError(f"Missing runtime README source: {source_readme}")
    (mixer_dir / "README.md").write_text(source_readme.read_text())


def _extract_sync_guide(overview_path: Path) -> str:
    """Extract sync guide block from workflow overview markers."""
    content = overview_path.read_text()
    pattern = re.compile(
        rf"{re.escape(SYNC_GUIDE_START)}\s*(.*?)\s*{re.escape(SYNC_GUIDE_END)}",
        re.DOTALL,
    )
    match = pattern.search(content)
    if not match:
        raise RuntimeError(
            f"Missing sync guide markers in {overview_path}: "
            f"{SYNC_GUIDE_START} ... {SYNC_GUIDE_END}"
        )
    return match.group(1).strip()


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)

    skill_content = source_skill.read_text().rstrip()
    existing_block = re.compile(
        rf"\n*{re.escape(SKILL_MANAGED_START)}.*?{re.escape(SKILL_MANAGED_END)}\n*",
        re.DOTALL,
    )
    skill_without_old = existing_block.sub("\n\n", skill_content).rstrip()

    overview_path = package_dir / "workflows" / "doc.md"
    final_content = skill_without_old
    if overview_path.exists():
        guide = _extract_sync_guide(overview_path)
        managed_block = f"{SKILL_MANAGED_START}\n{guide}\n{SKILL_MANAGED_END}\n"
        final_content = f"{skill_without_old}\n\n{managed_block}".rstrip()

    target_skill.write_text(final_content + "\n")
    print(f"  Skill synced: {source_skill} -> {target_skill}")


HOOK_COMMAND = "python3 -m mixersystem.hooks.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 = {}

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

        # Remove any existing mixer-owned entries
        entries = [e for e in entries if not (
            isinstance(e, dict) and e.get("command") == HOOK_COMMAND
        )]

        # Add our hook entry
        entries.append({"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)
