#!/usr/bin/env python3
"""Build a Claude Code plugin directory from the nWave source tree.

Assembles agents, commands, skills, DES module, hooks, and metadata into
a plugin layout:
  nWave/agents/nw-*.md  -> plugin/agents/nw-*.md
  nWave/tasks/nw/*.md   -> plugin/commands/nw/*.md   (tasks -> commands)
  nWave/skills/*/       -> plugin/skills/*/           (preserving structure)
  src/des/              -> plugin/scripts/des/         (imports rewritten)
  nWave/templates/*.json-> plugin/scripts/templates/   (DES runtime templates)
  pyproject.toml        -> plugin/.claude-plugin/plugin.json (version extraction)
  (generated)           -> plugin/hooks/hooks.json     (5 DES hook events)
  (generated)           -> plugin/scripts/des-hook     (thin shell wrapper)

Design: Pure function pipeline with frozen dataclasses and Result types.
No side effects in domain logic; IO confined to pipeline boundaries.

Usage:
    python scripts/build_plugin.py
    python scripts/build_plugin.py --project-root /path/to/project
    python scripts/build_plugin.py --output-dir /path/to/output
"""

from __future__ import annotations

import argparse
import ast
import json
import re
import shutil
import stat
import sys
from dataclasses import dataclass, field
from pathlib import Path


# ---------------------------------------------------------------------------
# DES Import Rewriting Patterns (shared with build_dist.py)
# ---------------------------------------------------------------------------

_FROM_PATTERN = re.compile(r"\bfrom\s+src\.des\b")
_IMPORT_PATTERN = re.compile(r"\bimport\s+src\.des\b")
_GENERAL_PATTERN = re.compile(r"\bsrc\.des\.")


# ---------------------------------------------------------------------------
# Domain Types (frozen, immutable)
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class BuildConfig:
    """Immutable configuration for the plugin build pipeline."""

    source_root: Path
    nwave_dir: Path
    des_dir: Path
    pyproject_path: Path
    output_dir: Path
    plugin_name: str = "nw"

    hook_template_override: dict | None = None

    @staticmethod
    def from_dict(config: dict) -> BuildConfig:
        """Create BuildConfig from a dictionary (test fixture compatibility)."""
        return BuildConfig(
            source_root=Path(config["source_root"]),
            nwave_dir=Path(config["nwave_dir"]),
            des_dir=Path(config["des_dir"]),
            pyproject_path=Path(config["pyproject_path"]),
            output_dir=Path(config["output_dir"]),
            plugin_name=config.get("plugin_name", "nw"),
            hook_template_override=config.get("hook_template_override"),
        )

    @staticmethod
    def from_project_root(project_root: Path, output_dir: Path) -> BuildConfig:
        """Create BuildConfig from a project root directory."""
        return BuildConfig(
            source_root=project_root,
            nwave_dir=project_root / "nWave",
            des_dir=project_root / "src" / "des",
            pyproject_path=project_root / "pyproject.toml",
            output_dir=output_dir,
        )


@dataclass(frozen=True)
class StepResult:
    """Result of a single pipeline step."""

    step_name: str
    count: int
    success: bool
    error: str | None = None

    @staticmethod
    def ok(step_name: str, count: int) -> StepResult:
        return StepResult(step_name=step_name, count=count, success=True)

    @staticmethod
    def fail(step_name: str, error: str) -> StepResult:
        return StepResult(step_name=step_name, count=0, success=False, error=error)


@dataclass(frozen=True)
class BuildResult:
    """Immutable result of the complete build pipeline."""

    output_dir: Path
    success: bool
    error: str | None = None
    metadata: dict = field(default_factory=dict)
    steps: tuple[StepResult, ...] = ()

    def is_success(self) -> bool:
        return self.success


@dataclass(frozen=True)
class ValidationResult:
    """Immutable result of plugin validation.

    Pure function output: no side effects, reports ALL errors at once.
    """

    success: bool
    errors: tuple[str, ...]
    sections: dict[str, bool]
    counts: dict[str, int]


# ---------------------------------------------------------------------------
# Pure Functions: Source Validation
# ---------------------------------------------------------------------------


def validate_source_tree(config: BuildConfig) -> str | None:
    """Validate that required source directories exist.

    Returns None on success, error message on failure.
    """
    agents_dir = config.nwave_dir / "agents"
    if not agents_dir.exists():
        return "Source tree is missing the agents directory"

    skills_dir = config.nwave_dir / "skills"
    if not skills_dir.exists():
        return "Source tree is missing the skills directory"

    commands_dir = config.nwave_dir / "tasks" / "nw"
    if not commands_dir.exists():
        return "Source tree is missing the commands directory (tasks/nw)"

    if not config.des_dir.exists():
        return "Source tree is missing the DES source directory"

    return None


# ---------------------------------------------------------------------------
# Pure Functions: Version Extraction
# ---------------------------------------------------------------------------


def read_version(pyproject_path: Path) -> tuple[str | None, str | None]:
    """Read version from pyproject.toml.

    Returns (version, None) on success, (None, error) on failure.
    """
    if not pyproject_path.exists():
        return None, "Cannot read version: pyproject.toml not found"

    try:
        try:
            import tomllib
        except ModuleNotFoundError:
            import tomli as tomllib  # type: ignore[no-redef]

        with open(pyproject_path, "rb") as f:
            data = tomllib.load(f)
        version = data.get("project", {}).get("version")
        if version is None:
            return None, "Cannot read version: no project.version in pyproject.toml"
        return version, None

    except ModuleNotFoundError:
        # Fallback: regex parse
        content = pyproject_path.read_text(encoding="utf-8")
        match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
        if match:
            return match.group(1), None
        return None, "Cannot read version: could not parse pyproject.toml"

    except Exception as exc:
        return None, f"Cannot read version: {exc}"


# ---------------------------------------------------------------------------
# Pure Functions: Metadata Generation
# ---------------------------------------------------------------------------


def generate_plugin_metadata(plugin_name: str, version: str) -> dict:
    """Generate plugin.json metadata content."""
    return {
        "name": plugin_name,
        "version": version,
        "description": (
            "nWave: AI-powered workflow framework — 23 agents, 98+ skills, "
            "TDD enforcement, and wave-based development methodology for Claude Code"
        ),
        "author": {
            "name": "nWave AI",
            "email": "hello@nwave.ai",
        },
        "homepage": "https://nwave.ai",
        "repository": "https://github.com/nwave-ai/nwave",
        "license": "MIT",
        "source": f"./plugins/{plugin_name}",
        "keywords": [
            "tdd",
            "workflow",
            "agents",
            "software-craft",
            "bdd",
            "outside-in-tdd",
            "code-quality",
        ],
    }


def generate_marketplace_catalog(plugin_name: str, version: str) -> dict:
    """Generate marketplace.json catalog for self-hosted distribution."""
    return {
        "name": MARKETPLACE_NAME,
        "owner": {
            "name": "nWave AI",
            "email": "hello@nwave.ai",
        },
        "metadata": {
            "description": (
                "nWave plugin marketplace — structured AI development "
                "with TDD enforcement for Claude Code"
            ),
            "version": version,
        },
        "plugins": [
            {
                "name": plugin_name,
                "source": PLUGIN_SOURCE_TEMPLATE.format(name=plugin_name),
                "description": (
                    "nWave: AI-powered workflow framework — 23 agents, 98+ skills, "
                    "TDD enforcement, and wave-based development methodology"
                ),
                "version": version,
                "category": PLUGIN_CATEGORY,
                "tags": [
                    "tdd",
                    "workflow",
                    "agents",
                    "software-craft",
                    "bdd",
                    "code-quality",
                ],
            }
        ],
    }


# ---------------------------------------------------------------------------
# Pipeline Steps: IO at Boundaries
# ---------------------------------------------------------------------------


def copy_agents(config: BuildConfig, plugin_dir: Path) -> StepResult:
    """Copy agent definitions from source to plugin directory."""
    source_dir = config.nwave_dir / "agents"
    dest_dir = plugin_dir / "agents"
    dest_dir.mkdir(parents=True, exist_ok=True)

    count = 0
    for md_file in sorted(source_dir.glob("nw-*.md")):
        shutil.copy2(md_file, dest_dir / md_file.name)
        count += 1

    if count == 0:
        return StepResult.fail("agents", "No agent files found in source")

    return StepResult.ok("agents", count)


def copy_commands(config: BuildConfig, plugin_dir: Path) -> StepResult:
    """Copy command definitions from tasks/nw/ to commands/nw/."""
    source_dir = config.nwave_dir / "tasks" / "nw"
    dest_dir = plugin_dir / "commands" / "nw"
    dest_dir.mkdir(parents=True, exist_ok=True)

    count = 0
    for md_file in sorted(source_dir.glob("*.md")):
        shutil.copy2(md_file, dest_dir / md_file.name)
        count += 1

    if count == 0:
        return StepResult.fail("commands", "No command files found in source")

    return StepResult.ok("commands", count)


def copy_skills(config: BuildConfig, plugin_dir: Path) -> StepResult:
    """Copy skill files preserving directory structure."""
    source_dir = config.nwave_dir / "skills"
    dest_dir = plugin_dir / "skills"
    dest_dir.mkdir(parents=True, exist_ok=True)

    count = 0
    for skill_group in sorted(source_dir.iterdir()):
        if skill_group.is_dir():
            shutil.copytree(
                skill_group, dest_dir / skill_group.name, dirs_exist_ok=True
            )
            count += len(list(skill_group.rglob("*.md")))

    if count == 0:
        return StepResult.fail("skills", "No skill files found in source")

    return StepResult.ok("skills", count)


# ---------------------------------------------------------------------------
# Pure Functions: DES Import Rewriting
# ---------------------------------------------------------------------------


def rewrite_des_imports(content: str) -> str:
    """Rewrite src.des imports to standalone des imports.

    Transforms:
      from src.des -> from des
      import src.des -> import des
      src.des. -> des.
    """
    result = _FROM_PATTERN.sub("from des", content)
    result = _IMPORT_PATTERN.sub("import des", result)
    result = _GENERAL_PATTERN.sub("des.", result)
    return result


def validate_python_syntax(content: str, filename: str) -> str | None:
    """Validate that content is syntactically valid Python.

    Returns None on success, error message on failure.
    """
    try:
        ast.parse(content, filename=filename)
        return None
    except SyntaxError as exc:
        return f"Import rewrite produced invalid syntax in {filename}: {exc}"


# ---------------------------------------------------------------------------
# Pure Functions: Hook Configuration Generation
# ---------------------------------------------------------------------------

_HOOK_COMMAND_TEMPLATE = (
    "PYTHONPATH=${{CLAUDE_PLUGIN_ROOT}}/scripts python3"
    " -m des.adapters.drivers.hooks.claude_code_hook_adapter {action}"
)

_HOOK_EVENTS: tuple[tuple[str, str], ...] = (
    ("PreToolUse", "pre-task"),
    ("PostToolUse", "post-tool-use"),
    ("SubagentStop", "subagent-stop"),
    ("SessionStart", "session-start"),
    ("SubagentStart", "subagent-start"),
)


def generate_hook_entries(
    command_template: str = _HOOK_COMMAND_TEMPLATE,
) -> list[dict[str, str]]:
    """Generate the list of hook event entries for hooks.json."""
    return [
        {
            "event": event,
            "command": command_template.format(action=action),
        }
        for event, action in _HOOK_EVENTS
    ]


def validate_hook_entries(entries: list[dict[str, str]]) -> str | None:
    """Validate that all hook entries have non-empty commands.

    Returns None on success, error message on failure.
    """
    for entry in entries:
        command = entry.get("command", "")
        if not command.strip():
            event = entry.get("event", "unknown")
            return f"Hook configuration error: empty command for event '{event}'"
    return None


# ---------------------------------------------------------------------------
# Pure Functions: Shell Wrapper Generation
# ---------------------------------------------------------------------------

_HOOK_WRAPPER_CONTENT = """\
#!/usr/bin/env python3
\"\"\"DES hook wrapper for Claude Code plugin.

Arguments originate from Claude Code runtime only — not from user input.
\"\"\"
import os
import sys

# Ensure the scripts/ directory is on PYTHONPATH so `import des` works
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir not in sys.path:
    sys.path.insert(0, script_dir)

from des.adapters.drivers.hooks.claude_code_hook_adapter import main

if __name__ == "__main__":
    main()
"""


# ---------------------------------------------------------------------------
# Pipeline Steps: DES Module, Templates, Hooks
# ---------------------------------------------------------------------------


def copy_des_module(config: BuildConfig, plugin_dir: Path) -> StepResult:
    """Copy src/des/ to plugin/scripts/des/, rewrite imports, clear __pycache__."""
    dest_dir = plugin_dir / "scripts" / "des"
    shutil.copytree(config.des_dir, dest_dir, dirs_exist_ok=True)

    # __pycache__ contains absolute paths from the build machine — ship source only
    for cache_dir in dest_dir.rglob("__pycache__"):
        if cache_dir.is_dir():
            shutil.rmtree(cache_dir)

    # src.des imports must become standalone des imports so the plugin
    # works without the nWave source tree on PYTHONPATH
    files_rewritten = 0
    for py_file in sorted(dest_dir.rglob("*.py")):
        original_content = py_file.read_text(encoding="utf-8")
        rewritten_content = rewrite_des_imports(original_content)

        if rewritten_content != original_content:
            syntax_error = validate_python_syntax(rewritten_content, py_file.name)
            if syntax_error is not None:
                return StepResult.fail("des_module", syntax_error)
            py_file.write_text(rewritten_content, encoding="utf-8")
            files_rewritten += 1

    return StepResult.ok("des_module", files_rewritten)


def copy_templates(config: BuildConfig, plugin_dir: Path) -> StepResult:
    """Copy DES runtime templates to plugin/scripts/templates/."""
    templates_dir = config.nwave_dir / "templates"
    dest_dir = plugin_dir / "scripts" / "templates"
    dest_dir.mkdir(parents=True, exist_ok=True)

    template_files = (
        "step-tdd-cycle-schema.json",
        "roadmap-schema.json",
    )

    count = 0
    for template_name in template_files:
        source_file = templates_dir / template_name
        if source_file.exists():
            shutil.copy2(source_file, dest_dir / template_name)
            count += 1

    return StepResult.ok("templates", count)


def generate_hooks_json(
    plugin_dir: Path, hook_template_override: dict | None = None
) -> StepResult:
    """Generate hooks/hooks.json with all 5 DES enforcement events.

    If hook_template_override is provided, its entries are validated and used
    instead of the default entries. This enables error-path testing.
    """
    if hook_template_override is not None:
        entries = hook_template_override.get("hooks", [])
        validation_error = validate_hook_entries(entries)
        if validation_error is not None:
            return StepResult.fail("hooks", validation_error)
    else:
        entries = generate_hook_entries()
        validation_error = validate_hook_entries(entries)
        if validation_error is not None:
            return StepResult.fail("hooks", validation_error)

    hooks_dir = plugin_dir / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)

    hooks_data = {"hooks": entries}
    hooks_path = hooks_dir / "hooks.json"
    hooks_path.write_text(json.dumps(hooks_data, indent=2) + "\n", encoding="utf-8")

    return StepResult.ok("hooks", len(entries))


def generate_hook_wrapper(plugin_dir: Path) -> StepResult:
    """Generate scripts/des-hook shell wrapper (< 10 lines)."""
    scripts_dir = plugin_dir / "scripts"
    scripts_dir.mkdir(parents=True, exist_ok=True)

    wrapper_path = scripts_dir / "des-hook"
    wrapper_path.write_text(_HOOK_WRAPPER_CONTENT, encoding="utf-8")
    # Make executable
    wrapper_path.chmod(
        wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH
    )

    return StepResult.ok("hook_wrapper", 1)


def write_metadata(plugin_dir: Path, metadata: dict) -> StepResult:
    """Write plugin.json metadata to the .claude-plugin directory."""
    metadata_dir = plugin_dir / ".claude-plugin"
    metadata_dir.mkdir(parents=True, exist_ok=True)

    metadata_path = metadata_dir / "plugin.json"
    metadata_path.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")

    return StepResult.ok("metadata", 1)


def write_marketplace_json(
    marketplace_dir: Path,
    plugin_name: str,
    version: str,
) -> StepResult:
    """Write marketplace.json for self-hosted distribution.

    Creates the directory structure expected by Claude Code:
      marketplace_dir/
        .claude-plugin/marketplace.json
        plugins/nwave/  -> symlink or copy of plugin artifacts
    """
    catalog = generate_marketplace_catalog(plugin_name, version)

    mp_meta_dir = marketplace_dir / ".claude-plugin"
    mp_meta_dir.mkdir(parents=True, exist_ok=True)

    mp_path = mp_meta_dir / "marketplace.json"
    mp_path.write_text(json.dumps(catalog, indent=2) + "\n", encoding="utf-8")

    return StepResult.ok("marketplace", 1)


# ---------------------------------------------------------------------------
# Pipeline Composition
# ---------------------------------------------------------------------------


def _cleanup_on_failure(plugin_dir: Path, *, created_by_build: bool) -> None:
    """Remove partial output on build failure.

    Only removes the directory if it was created by this build run,
    preventing accidental destruction of pre-existing content.
    """
    if created_by_build and plugin_dir.exists():
        shutil.rmtree(plugin_dir)


def build(config: BuildConfig) -> BuildResult:
    """Execute the plugin assembly pipeline.

    Pipeline: validate -> read_version -> copy_agents -> copy_commands
              -> copy_skills -> copy_des_module -> copy_templates
              -> generate_hooks_json -> generate_hook_wrapper
              -> generate_metadata -> write_metadata
    """
    # Step 1: Validate source tree
    source_error = validate_source_tree(config)
    if source_error is not None:
        return BuildResult(
            output_dir=config.output_dir,
            success=False,
            error=source_error,
        )

    # Step 2: Read and validate version
    version, read_error = read_version(config.pyproject_path)
    if read_error is not None:
        return BuildResult(
            output_dir=config.output_dir,
            success=False,
            error=read_error,
        )

    # Step 3: Prepare plugin directory
    plugin_dir = config.output_dir
    created_by_build = not plugin_dir.exists()
    plugin_dir.mkdir(parents=True, exist_ok=True)

    def _fail(error: str, steps_so_far: tuple[StepResult, ...]) -> BuildResult:
        _cleanup_on_failure(plugin_dir, created_by_build=created_by_build)
        return BuildResult(
            output_dir=config.output_dir,
            success=False,
            error=error,
            steps=steps_so_far,
        )

    # Step 4: Execute copy pipeline (agents, commands, skills)
    steps: list[StepResult] = []

    copy_functions = [copy_agents, copy_commands, copy_skills]
    for copy_fn in copy_functions:
        result = copy_fn(config, plugin_dir)
        steps.append(result)
        if not result.success:
            return _fail(result.error, tuple(steps))

    # Step 5: DES module bundling
    des_result = copy_des_module(config, plugin_dir)
    steps.append(des_result)
    if not des_result.success:
        return _fail(des_result.error, tuple(steps))

    # Step 6: DES runtime templates
    templates_result = copy_templates(config, plugin_dir)
    steps.append(templates_result)

    # Step 7: Hook configuration
    hooks_result = generate_hooks_json(plugin_dir, config.hook_template_override)
    steps.append(hooks_result)
    if not hooks_result.success:
        return _fail(hooks_result.error, tuple(steps))

    # Step 8: Hook shell wrapper
    wrapper_result = generate_hook_wrapper(plugin_dir)
    steps.append(wrapper_result)

    # Step 9: Generate and write metadata
    metadata = generate_plugin_metadata(config.plugin_name, version)
    metadata_result = write_metadata(plugin_dir, metadata)
    steps.append(metadata_result)

    return BuildResult(
        output_dir=plugin_dir,
        success=True,
        metadata=metadata,
        steps=tuple(steps),
    )


# ---------------------------------------------------------------------------
# Driving Port: Plugin Validation
# ---------------------------------------------------------------------------


def _validate_metadata(plugin_dir: Path) -> tuple[bool, list[str]]:
    """Validate .claude-plugin/plugin.json exists and is well-formed."""
    errors: list[str] = []
    metadata_path = plugin_dir / ".claude-plugin" / "plugin.json"

    if not metadata_path.exists():
        errors.append("Missing metadata: .claude-plugin/plugin.json not found")
        return False, errors

    try:
        data = json.loads(metadata_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as exc:
        errors.append(f"Invalid metadata: plugin.json is not valid JSON ({exc})")
        return False, errors

    if "name" not in data:
        errors.append("Invalid metadata: plugin.json missing required field 'name'")
    if "version" not in data:
        errors.append("Invalid metadata: plugin.json missing required field 'version'")

    return len(errors) == 0, errors


def _validate_directory_with_md_files(
    plugin_dir: Path, section: str
) -> tuple[bool, list[str], int]:
    """Validate a directory exists and contains at least one .md file."""
    errors: list[str] = []
    target_dir = plugin_dir / section

    if not target_dir.exists():
        errors.append(f"Missing {section}: {section}/ directory not found")
        return False, errors, 0

    md_files = list(target_dir.rglob("*.md"))
    count = len(md_files)

    if count == 0:
        errors.append(f"Empty {section}: {section}/ contains no .md files")
        return False, errors, 0

    return True, errors, count


def _validate_hooks(plugin_dir: Path) -> tuple[bool, list[str]]:
    """Validate hooks/hooks.json exists, is valid JSON, and has proper schema."""
    errors: list[str] = []
    hooks_path = plugin_dir / "hooks" / "hooks.json"

    if not hooks_path.exists():
        errors.append("Missing hooks: hooks/hooks.json not found")
        return False, errors

    try:
        data = json.loads(hooks_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as exc:
        errors.append(f"Invalid hooks: hooks.json is not valid JSON ({exc})")
        return False, errors

    if "hooks" not in data:
        errors.append("Invalid hooks: hooks.json missing required 'hooks' array")
        return False, errors

    if not isinstance(data["hooks"], list):
        errors.append("Invalid hooks: 'hooks' must be an array")
        return False, errors

    for idx, entry in enumerate(data["hooks"]):
        event = entry.get("event", "")
        command = entry.get("command", "")
        if not event or not isinstance(event, str):
            errors.append(f"Invalid hooks: entry {idx} missing or empty 'event' field")
        if not command or not isinstance(command, str):
            errors.append(
                f"Invalid hooks: entry {idx} missing or empty 'command' field"
            )

    return len(errors) == 0, errors


def _validate_des_module(plugin_dir: Path) -> tuple[bool, list[str]]:
    """Validate scripts/des/__init__.py exists."""
    errors: list[str] = []
    init_path = plugin_dir / "scripts" / "des" / "__init__.py"

    if not init_path.exists():
        errors.append("Missing DES module: scripts/des/__init__.py not found")
        return False, errors

    return True, errors


def generate_marketplace_manifest(
    plugin_dir: Path,
    download_url: str = "",
) -> StepResult:
    """Generate marketplace-manifest.json for self-hosted distribution."""
    metadata_path = plugin_dir / ".claude-plugin" / "plugin.json"
    if not metadata_path.exists():
        return StepResult.fail(
            "manifest", "Cannot generate manifest: plugin.json not found"
        )

    metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
    manifest = {
        "name": metadata.get("name", ""),
        "version": metadata.get("version", ""),
        "description": metadata.get("description", ""),
        "download": download_url,
        "homepage": "https://nwave.ai",
    }

    manifest_path = plugin_dir / "marketplace-manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")

    return StepResult.ok("manifest", 1)


def validate(plugin_dir: Path) -> ValidationResult:
    """Validate plugin structure is complete and well-formed.

    Pure function: reads plugin_dir, returns ValidationResult.
    Reports ALL errors, does not stop at first.
    """
    all_errors: list[str] = []
    sections: dict[str, bool] = {}
    counts: dict[str, int] = {}

    # Metadata validation
    metadata_ok, metadata_errors = _validate_metadata(plugin_dir)
    sections["metadata"] = metadata_ok
    all_errors.extend(metadata_errors)

    # Agents validation
    agents_ok, agents_errors, agents_count = _validate_directory_with_md_files(
        plugin_dir, "agents"
    )
    sections["agents"] = agents_ok
    counts["agents"] = agents_count
    all_errors.extend(agents_errors)

    # Skills validation
    skills_ok, skills_errors, skills_count = _validate_directory_with_md_files(
        plugin_dir, "skills"
    )
    sections["skills"] = skills_ok
    counts["skills"] = skills_count
    all_errors.extend(skills_errors)

    # Commands validation
    commands_ok, commands_errors, commands_count = _validate_directory_with_md_files(
        plugin_dir, "commands"
    )
    sections["commands"] = commands_ok
    counts["commands"] = commands_count
    all_errors.extend(commands_errors)

    # Hooks validation
    hooks_ok, hooks_errors = _validate_hooks(plugin_dir)
    sections["hooks"] = hooks_ok
    all_errors.extend(hooks_errors)

    # DES module validation
    des_ok, des_errors = _validate_des_module(plugin_dir)
    sections["des_module"] = des_ok
    all_errors.extend(des_errors)

    return ValidationResult(
        success=len(all_errors) == 0,
        errors=tuple(all_errors),
        sections=sections,
        counts=counts,
    )


# ---------------------------------------------------------------------------
# Pure Functions: Coexistence Verification
# ---------------------------------------------------------------------------

# Plugin installation paths (relative to ~/.claude/)
PLUGIN_INSTALL_PREFIX = "plugins/cache/nwave"

# Marketplace configuration
MARKETPLACE_NAME = "nwave-marketplace"
PLUGIN_SOURCE_TEMPLATE = "./plugins/{name}"
PLUGIN_CATEGORY = "development-workflows"


def get_plugin_paths() -> set[str]:
    """Return the relative paths owned by the plugin installation."""
    return {
        f"{PLUGIN_INSTALL_PREFIX}/agents",
        f"{PLUGIN_INSTALL_PREFIX}/commands",
        f"{PLUGIN_INSTALL_PREFIX}/skills",
        f"{PLUGIN_INSTALL_PREFIX}/hooks",
        f"{PLUGIN_INSTALL_PREFIX}/scripts",
        f"{PLUGIN_INSTALL_PREFIX}/.claude-plugin",
    }


def get_installer_paths() -> set[str]:
    """Return the relative paths owned by the custom installer."""
    return {
        "agents/nw",
        "commands/nw",
        "skills/nw",
        "lib/python/des",
        "scripts/des",
        "templates",
    }


def check_version_consistency(
    plugin_version: str, installer_version: str
) -> str | None:
    """Check if plugin and installer versions match.

    Returns None if versions match, warning message if they differ.
    """
    if plugin_version != installer_version:
        return (
            f"Version mismatch: plugin is {plugin_version}, "
            f"custom installer is {installer_version}. "
            f"Consider upgrading the older installation."
        )
    return None


def verify_path_disjointness() -> tuple[bool, list[str]]:
    """Verify plugin and installer paths never overlap.

    Returns (is_disjoint, overlapping_paths).
    """
    plugin_paths = get_plugin_paths()
    installer_paths = get_installer_paths()

    overlaps = []
    for pp in plugin_paths:
        for ip in installer_paths:
            if pp.startswith(ip) or ip.startswith(pp):
                overlaps.append(f"{pp} overlaps with {ip}")

    return len(overlaps) == 0, overlaps


# ---------------------------------------------------------------------------
# CLI Entry Point
# ---------------------------------------------------------------------------


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Build Claude Code plugin from nWave source"
    )
    parser.add_argument(
        "--project-root",
        type=Path,
        help="Project root directory (default: auto-detect)",
    )
    parser.add_argument(
        "--output-dir",
        type=Path,
        help="Output directory for plugin (default: plugin/)",
    )
    parser.add_argument(
        "--download-url",
        type=str,
        default="",
        help="Download URL for marketplace manifest",
    )
    args = parser.parse_args()

    project_root = args.project_root or Path(__file__).parent.parent
    output_dir = args.output_dir or project_root / "plugin"

    config = BuildConfig.from_project_root(project_root, output_dir)

    print(f"[INFO] Building plugin from {config.nwave_dir}")
    print(f"[INFO] Output: {config.output_dir}")

    result = build(config)

    for step in result.steps:
        status = "OK" if step.success else "FAIL"
        print(f"[{status}] {step.step_name}: {step.count} items")

    if result.success:
        print(
            f"[INFO] Plugin built successfully (version {result.metadata['version']})"
        )

        # Generate marketplace manifest after successful build
        manifest_result = generate_marketplace_manifest(
            result.output_dir, args.download_url
        )
        if manifest_result.success:
            print("[OK] manifest: 1 items")
        else:
            print(f"[WARN] Manifest generation failed: {manifest_result.error}")
    else:
        print(f"[ERROR] Build failed: {result.error}")
        sys.exit(1)


if __name__ == "__main__":
    main()
