#!/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 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)
    settings["modules"] = modules

    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)
