#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PASCAL = re.compile(r"^[A-Z][A-Za-z0-9]*$")
REQ = re.compile(r"^REQ-[A-Z0-9-]+$")
ROUTE = re.compile(r"^[a-z0-9][a-z0-9_/{}/.-]*$")
ALLOWED_METHODS = {"Get", "Post", "Put", "Patch", "Delete"}
ALLOWED_ROLES = {"Admin", "Advisor", "Approver", "Auditor", "Compliance", "DataOps", "Quant", "Risk", "System"}

@dataclass(frozen=True)
class Spec:
    module: str
    slice_name: str
    requirement_id: str
    http_method: str
    route: str
    role: str
    frontend_feature: str


def validate(spec: Spec) -> None:
    errors: list[str] = []
    if not PASCAL.fullmatch(spec.module): errors.append("module must be PascalCase")
    if not PASCAL.fullmatch(spec.slice_name): errors.append("slice must be PascalCase")
    if not REQ.fullmatch(spec.requirement_id): errors.append("requirement must match REQ-[A-Z0-9-]+")
    if spec.http_method not in ALLOWED_METHODS: errors.append(f"method must be one of {sorted(ALLOWED_METHODS)}")
    if not ROUTE.fullmatch(spec.route) or spec.route.startswith("/"): errors.append("route must be relative lower-case API path")
    if spec.role not in ALLOWED_ROLES: errors.append(f"role must be one of {sorted(ALLOWED_ROLES)}")
    if not re.fullmatch(r"[a-z][a-z0-9-]*", spec.frontend_feature): errors.append("feature must be kebab-case")
    if errors: raise ValueError("; ".join(errors))


def render(template: Path, values: dict[str, str]) -> str:
    text = template.read_text(encoding="utf-8")
    for key, value in values.items(): text = text.replace(f"__{key}__", value)
    unresolved = sorted(set(re.findall(r"__[A-Z0-9_]+__", text)))
    if unresolved: raise ValueError(f"unresolved template tokens in {template.name}: {unresolved}")
    return text


def plan(spec: Spec) -> dict[Path, str]:
    validate(spec)
    values = {
        "MODULE": f"KArtSell.Modules.{spec.module}",
        "SLICE": spec.slice_name,
        "REQUIREMENT_ID": spec.requirement_id,
        "HTTP_METHOD": spec.http_method,
        "ROUTE": spec.route,
        "ROLE": spec.role,
        "FEATURE": spec.frontend_feature,
    }
    backend_dir = ROOT / "src" / f"KArtSell.Modules.{spec.module}" / "Features" / spec.slice_name
    frontend_dir = ROOT / "frontend" / "src" / "features" / spec.frontend_feature
    files: dict[Path, str] = {}
    dotnet = ROOT / "templates" / "dotnet" / "VerticalSlice"
    for name in ["Endpoint.cs", "Request.cs", "Response.cs", "Handler.cs", "Validator.cs", "Sql.cs", "README.md"]:
        files[backend_dir / name] = render(dotnet / f"{name}.template", values)
    vue = ROOT / "templates" / "vue" / "Feature"
    for template_name, output_name in [
        ("api.ts.template", "api.ts"), ("queries.ts.template", "queries.ts"),
        ("schema.ts.template", "schema.ts"), ("page.vue.template", "pages/FeaturePage.vue"),
        ("schema.spec.ts.template", "tests/schema.spec.ts")]:
        files[frontend_dir / output_name] = render(vue / template_name, values)
    return files


def main() -> int:
    parser = argparse.ArgumentParser(description="Create a fail-closed Vertical Slice scaffold from reviewed templates.")
    parser.add_argument("--module", required=True)
    parser.add_argument("--slice", dest="slice_name", required=True)
    parser.add_argument("--requirement", required=True)
    parser.add_argument("--method", default="Post")
    parser.add_argument("--route", required=True)
    parser.add_argument("--role", required=True)
    parser.add_argument("--feature", required=True)
    parser.add_argument("--write", action="store_true", help="write files; otherwise print a deterministic plan")
    args = parser.parse_args()
    try:
        spec = Spec(args.module, args.slice_name, args.requirement, args.method, args.route, args.role, args.feature)
        files = plan(spec)
        collisions = [str(path.relative_to(ROOT)) for path in files if path.exists()]
        if collisions: raise FileExistsError("refusing to overwrite existing files: " + ", ".join(collisions))
        manifest = {str(path.relative_to(ROOT)): content for path, content in sorted(files.items())}
        if not args.write:
            print(json.dumps({"status": "DRY_RUN", "files": list(manifest)}, indent=2, ensure_ascii=False))
            return 0
        for path, content in files.items():
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content, encoding="utf-8")
        marker = ROOT / "docs" / "generated" / f"{spec.requirement_id}_{spec.slice_name}.json"
        marker.parent.mkdir(parents=True, exist_ok=True)
        marker.write_text(json.dumps({"status":"SCAFFOLD_ONLY","spec":spec.__dict__,"files":list(manifest)}, ensure_ascii=False, indent=2)+"\n", encoding="utf-8")
        print(json.dumps({"status": "WRITTEN", "files": list(manifest), "marker": str(marker.relative_to(ROOT))}, indent=2, ensure_ascii=False))
        return 0
    except (ValueError, FileExistsError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

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