#!/usr/bin/env python3
"""Install unnamespaced Claude Code aliases for CARBON plugin skills."""

from __future__ import annotations

import argparse
import json
import re
import shutil
import sys
from pathlib import Path


MARKER = "<!-- generated by CARBON bare Claude alias installer -->"
SKILL_NAME = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")


def arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Expose CARBON's Claude Code skills as /carbon, /carbon-auto, and other bare commands."
    )
    parser.add_argument(
        "--plugin-root",
        default=str(Path(__file__).resolve().parent.parent),
        help="CARBON plugin root containing skills/ (defaults to this script's plugin).",
    )
    parser.add_argument(
        "--skills-dir",
        default=str(Path.home() / ".claude" / "skills"),
        help="Claude user skills directory (default: ~/.claude/skills).",
    )
    parser.add_argument("--apply", action="store_true", help="Write aliases. Without this flag, preview only.")
    parser.add_argument("--remove", action="store_true", help="Remove aliases previously generated by this script.")
    parser.add_argument(
        "--force",
        action="store_true",
        help="Replace a same-named skill that was not generated by CARBON.",
    )
    return parser.parse_args()


def frontmatter_value(text: str, key: str) -> str:
    match = re.search(rf"^{re.escape(key)}:\s*(.+)$", text, re.MULTILINE)
    if not match:
        raise ValueError(f"missing {key} in skill frontmatter")
    value = match.group(1).strip()
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
        try:
            return str(json.loads(value)) if value[0] == '"' else value[1:-1]
        except json.JSONDecodeError:
            return value[1:-1]
    return value


def preferred_alias_name(canonical_name: str) -> str:
    if canonical_name == "carbon" or canonical_name.startswith("carbon-"):
        return canonical_name
    return f"carbon-{canonical_name}"


def alias_document(name: str, description: str, canonical: Path) -> str:
    source_dir = canonical.parent
    return f"""---
name: {name}
description: {json.dumps('Bare Claude Code command for CARBON. ' + description, ensure_ascii=False)}
argument-hint: "[scope or request]"
disable-model-invocation: true
---

# CARBON Claude command alias

{MARKER}

This is the unnamespaced `/{name}` alias for the installed CARBON workflow.

1. Read `{canonical}` completely before taking any task action.
2. Follow that canonical skill exactly, including every required reference and safety boundary.
3. Resolve its relative paths against `{source_dir}`, not this alias directory.
4. Pass the user's arguments through unchanged:

```text
$ARGUMENTS
```

5. Immediately after the required **CARBON will:** preview, call `carbon_command_progress action=start` with the exact active project root, the preview's high-level flow, plain-language reason, and known risks. Never omit `root` or let the MCP server default to its own working directory. Attach and request opening of its embedded project-local `text/html` resource in Claude's Artifacts pane without waiting for the user to click, then update its `progressId` after every major step with the outcome, confidence movement and basis, risks or unknowns, and privacy-approved evidence when available.
6. For every returned CARBON HTML page, attach the embedded `text/html` resource as a Claude artifact and open it in the Artifacts pane beside chat.
7. End with the exact absolute in-workspace `claudeChatLink` (or `claudeFileLink` when identical). Never substitute the generic relative `chatLink`, a `file://` URL, or the raw localhost URL; Claude can resolve a relative link against the wrong project root.
8. Before starting executable tests, call `carbon_open_execution_plan` with the exact active project root, request its returned live-plan artifact in the Claude side pane, and keep it open while execution updates it. Do not start execution first. MCP can request but cannot prove Claude opened the pane; never claim it opened unless the host visibly confirms it. If automatic opening is unavailable, leave the exact project-local link visible and state that host limitation.
9. Never invoke macOS `open`, `xdg-open`, `start`, Chrome, Safari, Edge, or another external browser unless the user explicitly requests that fallback.
"""


def skill_sources(plugin_root: Path) -> list[Path]:
    sources = sorted((plugin_root / "skills").glob("*/SKILL.md"))
    if not sources:
        raise ValueError(f"no skills found under {plugin_root / 'skills'}")
    return sources


def main() -> int:
    args = arguments()
    plugin_root = Path(args.plugin_root).expanduser().resolve()
    skills_dir = Path(args.skills_dir).expanduser().resolve()
    sources = skill_sources(plugin_root)
    actions: list[dict[str, str]] = []
    conflicts: list[str] = []
    desired_names = {
        preferred_alias_name(frontmatter_value(source.read_text(encoding="utf-8"), "name"))
        for source in sources
    }

    if not args.remove and skills_dir.exists():
        for stale_target in sorted(skills_dir.glob("*/SKILL.md")):
            stale_text = stale_target.read_text(encoding="utf-8")
            stale_name = stale_target.parent.name
            if MARKER in stale_text and stale_name not in desired_names:
                actions.append({"action": "remove-stale", "name": stale_name, "target": str(stale_target.parent)})
                if args.apply:
                    shutil.rmtree(stale_target.parent)

    for source in sources:
        text = source.read_text(encoding="utf-8")
        canonical_name = frontmatter_value(text, "name")
        name = preferred_alias_name(canonical_name)
        description = frontmatter_value(text, "description")
        if not SKILL_NAME.fullmatch(canonical_name):
            raise ValueError(f"unsafe or invalid skill name {canonical_name!r} in {source}")
        if not SKILL_NAME.fullmatch(name):
            raise ValueError(f"unsafe or invalid alias name {name!r} in {source}")
        target_dir = skills_dir / name
        target = target_dir / "SKILL.md"
        existing = target.read_text(encoding="utf-8") if target.exists() else ""

        if args.remove:
            if target.exists() and MARKER in existing:
                actions.append({"action": "remove", "name": name, "target": str(target_dir)})
                if args.apply:
                    shutil.rmtree(target_dir)
            continue

        if target.exists() and MARKER not in existing and not args.force:
            conflicts.append(str(target))
            continue

        document = alias_document(name, description, source.resolve())
        action = "update" if target.exists() else "create"
        actions.append({"action": action, "name": name, "target": str(target)})
        if args.apply:
            target_dir.mkdir(parents=True, exist_ok=True)
            temporary = target.with_suffix(".md.tmp")
            temporary.write_text(document, encoding="utf-8")
            temporary.replace(target)

    result = {
        "status": "blocked" if conflicts else ("applied" if args.apply else "preview"),
        "pluginRoot": str(plugin_root),
        "skillsDir": str(skills_dir),
        "aliases": len([action for action in actions if action["action"] != "remove-stale"]),
        "staleAliasesRemoved": len([action for action in actions if action["action"] == "remove-stale"]),
        "actions": actions,
        "conflicts": conflicts,
        "restartRequired": bool(args.apply and actions),
    }
    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 2 if conflicts else 0


if __name__ == "__main__":
    sys.exit(main())
