#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright 2024-2026 Lukasz Krzemien (biuro@softspark.eu)
# Source: https://github.com/softspark/ai-toolkit

"""Generate .opencode/commands/*.md files for opencode (https://opencode.ai).

User-invocable skills become opencode slash commands. Knowledge skills
(``user-invocable: false``) are excluded — they load automatically via
AGENTS.md context instead of `/` invocation.

Generated commands put the prompt in the markdown BODY. opencode reads the
body as the prompt template; the ``template`` frontmatter field is JSON-config
only and is ignored in .md command files. Files are prefixed ``ai-toolkit-``
for clean uninstall.

opencode commands: https://opencode.ai/docs/commands/
"""
from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from codex_skill_adapter import build_opencode_skill_text, is_codex_adapted_skill
from emission import skills_dir
from frontmatter import frontmatter_field

COMMAND_PREFIX = "ai-toolkit-"


def _skill_body(skill_file: Path) -> str:
    """Return the markdown body of a skill (content after frontmatter).

    For skills that rely on Claude-only orchestration primitives, route
    through the portable adapter so Claude-only placeholders and APIs become
    OpenCode-native, signature-free guidance.
    """
    if is_codex_adapted_skill(skill_file):
        adapted = build_opencode_skill_text(skill_file)
        # Strip the adapted frontmatter — we emit our own below
        parts = adapted.split("---", 2)
        return parts[2].lstrip("\n") if len(parts) >= 3 else adapted

    text = skill_file.read_text(encoding="utf-8")
    if not text.startswith("---"):
        return text
    parts = text.split("---", 2)
    return parts[2].lstrip("\n") if len(parts) >= 3 else text


def _render_opencode_command(skill_file: Path) -> str:
    """Render a single opencode command .md file from a user-invocable skill."""
    description = frontmatter_field(skill_file, "description")
    agent_field = frontmatter_field(skill_file, "agent")
    # opencode reads the command prompt from the markdown BODY of the file.
    # The frontmatter only defines command properties; the `template` field is
    # JSON-config-only and is ignored in .md command files.
    body = _skill_body(skill_file).rstrip()

    lines: list[str] = ["---"]
    if description:
        safe_desc = description.replace('"', "'")
        lines.append(f'description: "{safe_desc}"')
    if agent_field:
        # opencode accepts an `agent` frontmatter field pointing at a subagent
        lines.append(f"agent: {_map_agent_name(agent_field)}")
    lines.append("---")
    lines.append("")
    lines.append(body)
    lines.append("")
    return "\n".join(lines)


def _map_agent_name(value: str) -> str:
    """Map ai-toolkit agent names to opencode subagent names.

    Our opencode agents are installed with the ``ai-toolkit-`` prefix by
    ``generate_opencode_agents.py``, so rewrite here for consistency.
    """
    value = value.strip()
    if not value:
        return value
    if value.startswith(COMMAND_PREFIX):
        return value
    return f"{COMMAND_PREFIX}{value}"


def _is_user_invocable(skill_file: Path) -> bool:
    """Return True if the skill should be exposed as a `/` command."""
    invocable = frontmatter_field(skill_file, "user-invocable")
    if invocable:
        return invocable.lower() not in ("false", "0", "no")
    # Absence + `disable-model-invocation: true` = task skill (user-invocable)
    disable_model = frontmatter_field(skill_file, "disable-model-invocation")
    if disable_model and disable_model.lower() in ("true", "1", "yes"):
        return True
    # Default: NOT invocable — avoids exposing knowledge skills as commands
    return False


def _cleanup_stale(commands_out: Path) -> int:
    """Remove stale ai-toolkit-* command files whose source no longer exists or is no longer invocable."""
    if not commands_out.is_dir():
        return 0
    removed = 0
    for f in sorted(commands_out.glob(f"{COMMAND_PREFIX}*.md")):
        source_name = f.stem[len(COMMAND_PREFIX):]
        source = skills_dir / source_name / "SKILL.md"
        if not source.is_file() or not _is_user_invocable(source):
            f.unlink()
            removed += 1
    return removed


def generate(
    target_dir: Path, config_root: Path | None = None
) -> tuple[int, int]:
    """Write opencode command files and return (written, removed_stale).

    By default writes to ``target_dir/.opencode/commands/`` (project-local).
    Pass ``config_root=~/.config/opencode`` for the global layout, which
    lives directly under ``commands/`` (no ``.opencode/`` prefix).
    """
    base = config_root if config_root is not None else target_dir / ".opencode"
    commands_out = base / "commands"
    commands_out.mkdir(parents=True, exist_ok=True)

    written = 0
    for skill_dir in sorted(skills_dir.iterdir()):
        if skill_dir.name.startswith("_"):
            continue
        skill_file = skill_dir / "SKILL.md"
        if not skill_file.is_file():
            continue
        if not _is_user_invocable(skill_file):
            continue
        name = frontmatter_field(skill_file, "name")
        if not name:
            continue
        out_path = commands_out / f"{COMMAND_PREFIX}{name}.md"
        out_path.write_text(_render_opencode_command(skill_file), encoding="utf-8")
        written += 1

    removed = _cleanup_stale(commands_out)
    return written, removed


def main() -> None:
    target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
    written, removed = generate(target)
    msg = f"Generated: .opencode/commands/ ({written} commands"
    if removed:
        msg += f", {removed} stale removed"
    msg += ")"
    print(msg)


if __name__ == "__main__":
    main()
