#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# tools/new-asset -- creates a skill or a subagent in a set from a template that passes validation as written.
#
# ```bash
# python3 tools/new-asset <set> skill <name> [--root DIR] [--description TEXT]
# python3 tools/new-asset <set> subagent <name> [--root DIR] [--description TEXT]
# ```
#
# A skill is `sets/<set>/skills/<name>/SKILL.md`; a subagent is `sets/<set>/agents/<name>.md`. The name is held to the
# name rules before a file is written: the grammar, and the set's prefix (`ai-tools-` in `core`, `<set>-` elsewhere),
# since an authored asset carries it; a vendored asset is copied in with its `UPSTREAM.conf` and is not created here.
# The description is written as a double-quoted frontmatter scalar with `\` and `"` escaped, so any one line of text
# -- `true`, `a: b`, a date -- validates as written; an empty one is refused, since `description` is required.
# Standard library only.
from __future__ import annotations

import argparse
import os
import pathlib
import sys

TOOLS = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(TOOLS / "lib"))

import asset_format as fmt  # noqa: E402
from findings import FindingCollector, refuse_unconverted_errors  # noqa: E402
from safe_read import RefusedRead  # noqa: E402
from safe_write import create_file  # noqa: E402

SKILL_TEMPLATE = """---
name: {name}
description: {description}
---

# {title}

What the skill does, as instructions the agent follows. Name a script through
its interpreter, relative to this directory: `python3 scripts/<tool>.py`.
"""

SUBAGENT_TEMPLATE = """---
name: {name}
description: {description}
---

You are a subagent. State the role, what it reads, and what it returns.
"""


def quoted_scalar(text: str) -> str:
    """`text` as the double-quoted scalar of the frontmatter subset: `\\` and `"` escaped, nothing else."""
    return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'


def main() -> int:
    parser = argparse.ArgumentParser(description="Create a skill or a subagent in a set.")
    parser.add_argument("set", help="the set the asset joins")
    parser.add_argument("kind", choices=("skill", "subagent"))
    parser.add_argument("name", help="the asset name, carrying the set's prefix")
    parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path("."),
                        help="the publisher repository (default: the current directory)")
    parser.add_argument("--description", default="What the asset does, then when to use it, in the third person.",
                        help="the frontmatter description, one line")
    args = parser.parse_args()
    root = args.root.resolve()
    collector = FindingCollector("new-asset")
    return refuse_unconverted_errors(collector, "repo.layout", root, lambda: create_asset(args, root, collector))


def create_asset(args: argparse.Namespace, root: pathlib.Path, collector: FindingCollector) -> int:
    set_directory = root / "sets" / args.set
    if not (set_directory / fmt.SET_CONF_FILE).is_file():
        collector.refuse(f"sets/{args.set}", "set.conf.missing", "no such set; create it with new-set")
        return collector.report()
    kind_directory = "skills" if args.kind == "skill" else "agents"
    display = f"sets/{args.set}/{kind_directory}/{args.name}" + ("" if args.kind == "skill" else ".md")
    if not fmt.is_valid_name(args.name):
        collector.refuse(display, "name.grammar", f"`{args.name}` is not 1-64 characters of a-z, 0-9 and single "
                                                  "hyphens, starting and ending with a letter or digit")
    for word in fmt.CLAUDE_RESERVED_WORDS:
        if word in args.name:
            collector.refuse(display, "name.reserved-claude", f"`{args.name}` contains `{word}`, which Claude reserves")
    prefix = fmt.authored_asset_prefix(args.set)
    if not args.name.startswith(prefix):
        collector.refuse(display, "name.asset-prefix", f"an authored asset of `{args.set}` is named `{prefix}<name>`")
    if "\n" in args.description or len(args.description) > fmt.DESCRIPTION_MAX_LENGTH:
        collector.refuse(display, "frontmatter.length",
                         f"the description is one line of at most {fmt.DESCRIPTION_MAX_LENGTH} characters")
    elif not args.description.strip():
        collector.refuse(display, "frontmatter.required", "the description is non-empty text")
    target = pathlib.PurePath("sets", args.set, kind_directory, args.name if args.kind == "skill" else f"{args.name}.md")
    if os.path.lexists(root / target):
        collector.refuse(display, "kind.shape", "exists already")
    if collector.has_refusals():
        return collector.report()
    title = args.name[len(prefix):].replace("-", " ") or args.name
    values = {"name": args.name, "description": quoted_scalar(args.description), "title": title}
    # The components under the root are checked and the file is created exclusively, so a link at `sets/`, at the
    # kind directory or dangling at the asset's own name is refused instead of written through.
    try:
        if args.kind == "skill":
            create_file(root, target / "SKILL.md", SKILL_TEMPLATE.format(**values).encode("utf-8"))
            print(f"new-asset: wrote {display}/SKILL.md")
        else:
            create_file(root, target, SUBAGENT_TEMPLATE.format(**values).encode("utf-8"))
            print(f"new-asset: wrote {display}")
    except RefusedRead as refusal:
        collector.refuse(display, refusal.rule_id, refusal.message)
    return collector.report()


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