# Perseus Install Module (Phase 24)
# ────────────────────────────────────

import json
import os
import sys
from pathlib import Path
from typing import Optional

from perseus.assistant_formats import FORMAT_TARGETS, wrap_rendered


# ── Hook templates per target ────────────────────────────────────────────────

def _claude_code_hooks(perseus_cmd: str = "perseus", source: str = ".perseus/context.md") -> dict:
    """Return the hooks dict to merge into .claude/settings.json."""
    cmd = f"{perseus_cmd} render {source}"
    return {
        "SessionStart": [
            {
                "matcher": "",
                "hooks": [
                    {"type": "command", "command": cmd}
                ],
            }
        ],
        "UserPromptSubmit": [
            {
                "matcher": "",
                "hooks": [
                    {"type": "command", "command": cmd}
                ],
            }
        ],
    }


def _cursor_rules_block(version: str) -> str:
    """Return text to append to .cursorrules."""
    return (
        f"\n\n# ── Perseus context (auto-generated, do not edit below this line) ──\n"
        f"# Run `perseus render --format cursorrules` to regenerate.\n"
        f"# Source: .perseus/context.md\n"
        f"# v{version}\n"
    )


def _copilot_header(version: str) -> str:
    """Return header for .github/copilot-instructions.md."""
    return (
        f"<!-- Generated by Perseus v{version} — edit .perseus/context.md instead. -->\n"
        f"<!-- Run `perseus render --format copilot-instructions` to regenerate. -->\n\n"
    )


# ── Target detection and installation ────────────────────────────────────────

def _find_project_root(start: Path | None = None) -> Path:
    """Walk upward from start to find a project root.

    Heuristic: a directory containing .git/, .perseus/, or .claude/.
    Falls back to cwd.
    """
    current = (start or Path.cwd()).resolve()
    for _ in range(10):
        if (current / ".git").exists() or (current / ".perseus").is_dir():
            return current
        if current.parent == current:
            break
        current = current.parent
    return (start or Path.cwd()).resolve()


def _ensure_context_md(workspace: Path, cfg: dict) -> Path:
    """Ensure .perseus/context.md exists; scaffold if missing."""
    perseus_dir = workspace / ".perseus"
    context_file = perseus_dir / "context.md"

    if context_file.exists():
        return context_file

    if not perseus_dir.exists():
        perseus_dir.mkdir(parents=True, exist_ok=True)

    # Scaffold a minimal context.md
    content = (
        "@perseus v{version}\n"
        "\n"
        "# Project Context\n"
        "\n"
        "@query git branch --show-current\n"
        "@query git log --oneline -5\n"
        "@waypoint\n"
    ).format(
        version=cfg.get("version", "1.0.0")
    )
    context_file.write_text(content, encoding="utf-8")
    print(f"  ✓ Created {context_file}")
    return context_file


def _merge_json_file(path: Path, new_data: dict, top_key: str = "hooks") -> bool:
    """Merge new_data into an existing JSON file, deep-merging under top_key."""
    existing: dict = {}
    if path.exists():
        # H-10: Symlink check — refuse to overwrite symlinks
        if path.is_symlink():
            print(
                f"  ⚠ {path} is a symlink; refusing to overwrite for safety.",
                file=sys.stderr,
            )
            return False

        try:
            existing = json.loads(path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, ValueError):
            print(
                f"  ⚠ {path} exists but is not valid JSON; refusing to overwrite.",
                file=sys.stderr,
            )
            return False

        # H-10: JSON validation — refuse to merge into non-dict JSON
        if not isinstance(existing, dict):
            print(
                f"  ⚠ {path} contains non-dict JSON (type={type(existing).__name__}); "
                f"refusing to overwrite.",
                file=sys.stderr,
            )
            return False

    # Deep-merge hooks dicts
    if top_key not in existing:
        existing[top_key] = {}

    for event, hook_list in new_data.items():
        if event not in existing[top_key]:
            existing[top_key][event] = []
        # Avoid exact duplicates
        for hook_entry in hook_list:
            if hook_entry not in existing[top_key][event]:
                existing[top_key][event].append(hook_entry)

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
    return True


# ── Public API ───────────────────────────────────────────────────────────────

def install_target(
    target: str,
    cfg: dict,
    workspace: Path | None = None,
    perseus_cmd: str = "perseus",
    dry_run: bool = False,
) -> dict:
    """Install Perseus hooks for the given target assistant.

    Returns a dict with status info suitable for JSON or human display.
    """
    root = _find_project_root(workspace)
    version = cfg.get("version", "1.0.0")
    source = ".perseus/context.md"

    result: dict = {
        "target": target,
        "workspace": str(root),
        "actions": [],
        "dry_run": dry_run,
    }

    # ── Claude Code ──
    if target == "claude-code":
        # Ensure source exists
        ctx = _ensure_context_md(root, cfg)
        hooks = _claude_code_hooks(perseus_cmd=perseus_cmd)
        settings_path = root / ".claude" / "settings.json"

        result["actions"].append({
            "action": "merge_hooks",
            "file": str(settings_path),
            "events": list(hooks.keys()),
        })

        if not dry_run:
            _merge_json_file(settings_path, hooks)
            print(f"  ✓ Installed SessionStart + UserPromptSubmit hooks → {settings_path}")
            print(f"  → Source: {ctx}")

        result["actions"].append({
            "action": "ensure_source",
            "file": str(ctx),
        })

    # ── Cursor ──
    elif target == "cursor":
        ctx = _ensure_context_md(root, cfg)
        rules_path = root / ".cursorrules"
        block = _cursor_rules_block(version)

        result["actions"].append({
            "action": "append_block",
            "file": str(rules_path),
        })

        if not dry_run:
            existing = ""
            if rules_path.exists():
                existing = rules_path.read_text(encoding="utf-8")
            # Only append the block once
            if "Perseus context (auto-generated)" not in existing:
                rules_path.write_text(existing + block, encoding="utf-8")
                print(f"  ✓ Added Perseus block → {rules_path}")
            else:
                print(f"  • Perseus block already present in {rules_path}")

        result["actions"].append({
            "action": "ensure_source",
            "file": str(ctx),
        })

    # ── Gemini CLI ──
    elif target == "gemini-cli":
        ctx = _ensure_context_md(root, cfg)
        result["actions"].append({
            "action": "note",
            "text": (
                "Gemini CLI reads GEMINI_SYSTEM_MD environment variable. "
                "Set it to the Perseus-rendered output:\n"
                f"  export GEMINI_SYSTEM_MD=\"$(perseus render {source})\"\n"
                "Or add this to your shell profile."
            ),
        })
        if not dry_run:
            print(f"  ✓ Source ready: {ctx}")
            print(f"  → Add to your shell profile:")
            print(f'    export GEMINI_SYSTEM_MD="$(perseus render {source})"')

        result["actions"].append({
            "action": "ensure_source",
            "file": str(ctx),
        })

    # ── GitHub Copilot ──
    elif target == "copilot":
        ctx = _ensure_context_md(root, cfg)
        copilot_dir = root / ".github"
        copilot_file = copilot_dir / "copilot-instructions.md"

        result["actions"].append({
            "action": "write_or_update",
            "file": str(copilot_file),
        })

        if not dry_run:
            copilot_dir.mkdir(parents=True, exist_ok=True)
            header = _copilot_header(version)
            if not copilot_file.exists() or "Generated by Perseus" not in copilot_file.read_text(encoding="utf-8"):
                copilot_file.write_text(
                    header + f"Run `perseus render --format copilot-instructions` to populate this file.\n",
                    encoding="utf-8",
                )
                print(f"  ✓ Created {copilot_file}")
            else:
                print(f"  • Perseus header already present in {copilot_file}")

        result["actions"].append({
            "action": "ensure_source",
            "file": str(ctx),
        })

    else:
        print(f"  ✗ Unknown target: {target}", file=sys.stderr)
        print(f"    Supported: claude-code, cursor, gemini-cli, copilot", file=sys.stderr)
        result["error"] = f"Unknown target: {target}"
        return result

    # ── Render initial output ──
    if not dry_run and target in FORMAT_TARGETS:
        # Suggest the user run render
        fmt = {
            "claude-code": "claude-md",
            "cursor": "cursorrules",
            "gemini-cli": "claude-md",  # closest
            "copilot": "copilot-instructions",
        }.get(target, "agents-md")

        print(f"  → Run `perseus render {source} --format {fmt}` to populate the target file.")
        print(f"  → Or `perseus render {source} --format {fmt} -o {root / FORMAT_TARGETS[fmt].default_output}`")

    return result
