#!/usr/bin/env python3
"""FlowSpec CLI.

Dependency-free first version for workspace status and validation.
"""

from __future__ import annotations

import argparse
import fnmatch
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Iterable


PACKAGE_NAME = "@qdama/flowspec"
NPM_REGISTRY = "https://registry.npmjs.org/"
UPDATE_CHECK_TTL_SECONDS = 24 * 60 * 60
BLOCKING_SEVERITIES = {"error", "human-needed"}
FLOW_DIRS = [
    "flowspec/issues",
    "flowspec/mrs",
    "flowspec/evidence",
    "flowspec/release",
    "flowspec/archive",
]
HUMAN_GATE_PATTERNS = [
    "pom.xml",
    "package.json",
    "pnpm-lock.yaml",
    "package-lock.json",
    "yarn.lock",
    "requirements.txt",
    "deployment/*/sql/*",
    "deployment/*/nacos/*",
    "**/db/migration/*",
    "**/migrations/*",
    "**/*security*",
    "**/*permission*",
    "**/*auth*",
]


@dataclass
class ValidationIssue:
    severity: str
    code: str
    message: str
    fix: str | None = None
    path: str | None = None

    def to_dict(self) -> dict[str, Any]:
        data = asdict(self)
        return {key: value for key, value in data.items() if value is not None}


def run(command: list[str], cwd: Path) -> tuple[int, str, str]:
    proc = subprocess.run(
        command,
        cwd=str(cwd),
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )
    return proc.returncode, proc.stdout.strip(), proc.stderr.strip()


def package_root() -> Path:
    return Path(__file__).resolve().parents[1]


def current_version() -> str:
    package = read_json(package_root() / "package.json")
    version = package.get("version")
    return version if isinstance(version, str) else "0.0.0"


def version_parts(version: str) -> tuple[int, ...]:
    clean = version.split("-", 1)[0]
    parts: list[int] = []
    for piece in clean.split("."):
        try:
            parts.append(int(piece))
        except ValueError:
            parts.append(0)
    return tuple(parts)


def is_newer_version(latest: str, current: str) -> bool:
    latest_parts = version_parts(latest)
    current_parts = version_parts(current)
    size = max(len(latest_parts), len(current_parts))
    return latest_parts + (0,) * (size - len(latest_parts)) > current_parts + (0,) * (size - len(current_parts))


def config_dir() -> Path:
    root = os.environ.get("XDG_CONFIG_HOME")
    if root:
        return Path(root) / "flowspec"
    return Path.home() / ".config" / "flowspec"


def update_cache_file() -> Path:
    return config_dir() / "update-check.json"


def should_skip_update_check(args: argparse.Namespace) -> bool:
    if os.environ.get("FLOWSPEC_NO_UPDATE_CHECK") in {"1", "true", "TRUE", "yes", "YES"}:
        return True
    if getattr(args, "command", None) in {"update", "update-check"}:
        return True
    if getattr(args, "json", False):
        return True
    return not (sys.stdin.isatty() and sys.stderr.isatty())


def should_check_latest(force: bool = False) -> bool:
    if force:
        return True
    cache = read_json(update_cache_file())
    checked_at = cache.get("checked_at")
    if not isinstance(checked_at, (int, float)):
        return True
    return time.time() - float(checked_at) >= UPDATE_CHECK_TTL_SECONDS


def write_update_cache(latest: str | None = None) -> None:
    data: dict[str, Any] = {"checked_at": time.time()}
    if latest:
        data["latest"] = latest
    path = update_cache_file()
    path.parent.mkdir(parents=True, exist_ok=True)
    write_json(path, data)


def fetch_latest_version(timeout: float = 2.5) -> str | None:
    url = f"{NPM_REGISTRY.rstrip('/')}/{urllib.parse.quote(PACKAGE_NAME, safe='')}/latest"
    request = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "flowspec"})
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            payload = json.loads(response.read().decode("utf-8"))
    except (OSError, urllib.error.URLError, json.JSONDecodeError):
        return None
    version = payload.get("version") if isinstance(payload, dict) else None
    return version if isinstance(version, str) else None


def update_command() -> list[str]:
    return ["npm", "install", "-g", f"{PACKAGE_NAME}@latest", f"--registry={NPM_REGISTRY}"]


def run_update() -> int:
    print("Updating FlowSpec...", file=sys.stderr)
    proc = subprocess.run(update_command(), check=False)
    if proc.returncode == 0:
        print("FlowSpec updated. Command: flowspec update", file=sys.stderr)
    else:
        print("FlowSpec update failed. You can retry with: flowspec update", file=sys.stderr)
    return proc.returncode


def maybe_prompt_for_update(args: argparse.Namespace) -> None:
    if should_skip_update_check(args) or not should_check_latest():
        return
    latest = fetch_latest_version()
    if latest is None:
        return
    write_update_cache(latest)
    current = current_version()
    if not is_newer_version(latest, current):
        return

    print(
        f"\nFlowSpec {latest} is available. You are using {current}.\n"
        "Update command: flowspec update\n"
        "Update now? [Y/n] ",
        end="",
        file=sys.stderr,
        flush=True,
    )
    answer = sys.stdin.readline().strip().lower()
    if answer in {"", "y", "yes"}:
        run_update()
    else:
        print("Skipped. Run `flowspec update` when you are ready.", file=sys.stderr)


def find_work_dir(start: Path) -> Path:
    current = start.resolve()
    if current.is_file():
        current = current.parent
    for candidate in [current, *current.parents]:
        if (candidate / "manifest.json").exists():
            return candidate
    raise SystemExit(f"No manifest.json found from {start}")


def find_existing_work_dir(start: Path) -> Path | None:
    current = start.resolve()
    if current.is_file():
        current = current.parent
    for candidate in [current, *current.parents]:
        if (candidate / "manifest.json").exists():
            return candidate
    return None


def read_json(path: Path) -> dict[str, Any]:
    try:
        with path.open("r", encoding="utf-8") as handle:
            data = json.load(handle)
    except FileNotFoundError:
        return {}
    except json.JSONDecodeError as exc:
        raise SystemExit(f"Invalid JSON in {path}: {exc}") from exc
    if not isinstance(data, dict):
        raise SystemExit(f"Expected object JSON in {path}")
    return data


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def write_text_if_missing(path: Path, content: str) -> None:
    if path.exists():
        return
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def rel(path: Path, base: Path) -> str:
    try:
        return path.resolve().relative_to(base.resolve()).as_posix()
    except ValueError:
        return str(path)


def list_files(root: Path, pattern: str) -> list[Path]:
    return sorted(root.glob(pattern))


def git_status_paths(repo: Path) -> list[str]:
    code, out, _ = run(["git", "status", "--porcelain"], repo)
    if code != 0 or not out:
        return []
    paths: list[str] = []
    for line in out.splitlines():
        raw = line[3:] if len(line) > 3 else line
        if " -> " in raw:
            raw = raw.split(" -> ", 1)[1]
        paths.append(raw.strip())
    return paths


def worktree_git_root(path: Path) -> Path | None:
    code, out, _ = run(["git", "rev-parse", "--show-toplevel"], path)
    if code != 0 or not out:
        return None
    return Path(out)


def allowed_services(manifest: dict[str, Any]) -> set[str]:
    services = manifest.get("writable_services", [])
    if not isinstance(services, list):
        return set()
    names = set()
    for item in services:
        if isinstance(item, dict) and isinstance(item.get("name"), str):
            names.add(item["name"])
    return names


def readonly_services(manifest: dict[str, Any]) -> set[str]:
    services = manifest.get("readonly_services", [])
    if not isinstance(services, list):
        return set()
    names = set()
    for item in services:
        if isinstance(item, dict) and isinstance(item.get("name"), str):
            names.add(item["name"])
    return names


def validate_structure(work_dir: Path, manifest: dict[str, Any]) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    change_id = manifest.get("change")

    if not change_id:
        issues.append(
            ValidationIssue(
                "error",
                "manifest.change.missing",
                "manifest.json must contain change.",
                "Regenerate or update the FlowSpec workspace manifest.",
                "manifest.json",
            )
        )

    if not (work_dir / "AGENTS.md").exists():
        issues.append(
            ValidationIssue(
                "error",
                "workspace.agents.missing",
                "AGENTS.md is required for writable/read-only boundaries.",
                "Regenerate the workspace with flowspec-change.",
                "AGENTS.md",
            )
        )

    gitignore = work_dir / ".gitignore"
    if not gitignore.exists():
        issues.append(
            ValidationIssue(
                "warning",
                "workspace.gitignore.missing",
                ".gitignore should prevent services/ symlinks and local tool state from entering workspace Git.",
                "Regenerate the workspace or add /services/ to .gitignore.",
                ".gitignore",
            )
        )
    else:
        text = gitignore.read_text(encoding="utf-8", errors="ignore")
        if "services/" not in text and "/services/" not in text:
            issues.append(
                ValidationIssue(
                    "warning",
                    "workspace.gitignore.services_missing",
                    ".gitignore should ignore services/ symlink entries.",
                    "Add /services/ to .gitignore.",
                    ".gitignore",
                )
            )

    openspec_change = manifest.get("openspec_change")
    if not isinstance(openspec_change, str):
        issues.append(
            ValidationIssue(
                "error",
                "manifest.openspec_change.missing",
                "manifest.json must point to the OpenSpec change directory.",
                "Set manifest.openspec_change to work/<change-id>/openspec/changes/<change-id>.",
                "manifest.json",
            )
        )
    else:
        change_path = Path(openspec_change)
        if not change_path.is_absolute():
            change_path = work_dir / change_path
        if not change_path.exists():
            issues.append(
                ValidationIssue(
                    "error",
                    "openspec.change.missing",
                    "manifest.openspec_change does not exist.",
                    "Create or restore OpenSpec artifacts for this change.",
                    openspec_change,
                )
            )
        elif change_id and change_path.name != change_id:
            issues.append(
                ValidationIssue(
                    "error",
                    "openspec.change.mismatch",
                    "manifest.openspec_change must end with the same change id.",
                    "Update manifest.json or move the OpenSpec change folder.",
                    openspec_change,
                )
            )

    for required in FLOW_DIRS:
        if not (work_dir / required).exists():
            issues.append(
                ValidationIssue(
                    "warning",
                    "flowspec.dir.missing",
                    f"{required} is missing.",
                    "Run flowspec-change to initialize delivery artifact directories.",
                    required,
                )
            )

    services_dir = work_dir / "services"
    if services_dir.exists():
        for child in services_dir.iterdir():
            if not child.is_symlink():
                issues.append(
                    ValidationIssue(
                        "human-needed",
                        "service.not_symlink",
                        "Service entries must be symlinks to real repositories, not copied folders.",
                        "Replace copied service folders with symlinks.",
                        rel(child, work_dir),
                    )
                )

    return issues


def validate_openspec(work_dir: Path, manifest: dict[str, Any]) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    change_id = manifest.get("change")
    openspec_change = manifest.get("openspec_change")
    if not isinstance(change_id, str) or not isinstance(openspec_change, str):
        return issues
    change_path = Path(openspec_change)
    if not change_path.is_absolute():
        change_path = work_dir / change_path
    required_files = ["proposal.md", "tasks.md"]
    for filename in required_files:
        if not (change_path / filename).exists():
            issues.append(
                ValidationIssue(
                    "error",
                    "openspec.artifact.missing",
                    f"OpenSpec artifact {filename} is missing.",
                    "Run OpenSpec propose/continue before FlowSpec automation.",
                    str(change_path / filename),
                )
            )
    if not list_files(change_path, "specs/*/spec.md"):
        issues.append(
            ValidationIssue(
                "error",
                "openspec.specs.missing",
                "At least one OpenSpec delta spec is required.",
                "Create specs/<capability>/spec.md for the change.",
                str(change_path / "specs"),
            )
        )

    code, _, err = run(["openspec", "validate", change_id], work_dir)
    if code != 0:
        issues.append(
            ValidationIssue(
                "error",
                "openspec.validate.failed",
                "openspec validate failed.",
                err or "Run openspec validate manually for details.",
                "openspec",
            )
        )
    return issues


def validate_links(work_dir: Path, manifest: dict[str, Any]) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    for field in ["workspace", "work_dir", "catalog", "workspace_path"]:
        value = manifest.get(field)
        if not isinstance(value, str) or not value:
            issues.append(
                ValidationIssue(
                    "warning",
                    f"manifest.{field}.missing",
                    f"manifest.json should contain {field}.",
                    "Refresh manifest metadata with flowspec-change.",
                    "manifest.json",
                )
            )

    if not manifest.get("workspace_repo"):
        issues.append(
            ValidationIssue(
                "warning",
                "manifest.workspace_repo.missing",
                "No active workspace repo is recorded.",
                "Run flowspec init with --workspace-repo.",
                "manifest.json",
            )
        )

    if not manifest.get("main_issue"):
        issues.append(
            ValidationIssue(
                "warning",
                "manifest.main_issue.missing",
                "No main GitLab issue is linked yet.",
                "Link or create the main issue before MR-ready checks.",
                "manifest.json",
            )
        )

    for service in manifest.get("writable_services", []):
        if not isinstance(service, dict):
            continue
        if not service.get("name") or not service.get("work_path") or not service.get("target"):
            issues.append(
                ValidationIssue(
                    "error",
                    "manifest.service.incomplete",
                    "Each writable service needs name, target, and work_path.",
                    "Regenerate service metadata from catalog.",
                    "manifest.json",
                )
            )
    service_projects = manifest.get("service_projects")
    if service_projects is not None and not isinstance(service_projects, list):
        issues.append(
            ValidationIssue(
                "error",
                "manifest.service_projects.invalid",
                "manifest.service_projects must be a list.",
                "Regenerate manifest service project metadata.",
                "manifest.json",
            )
        )
    mrs = manifest.get("mrs")
    if mrs is not None and not isinstance(mrs, list):
        issues.append(
            ValidationIssue(
                "error",
                "manifest.mrs.invalid",
                "manifest.mrs must be a list.",
                "Regenerate MR metadata.",
                "manifest.json",
            )
        )
    return issues


def load_issue_files(work_dir: Path) -> list[tuple[Path, dict[str, Any]]]:
    files: list[tuple[Path, dict[str, Any]]] = []
    for path in sorted((work_dir / "flowspec" / "issues").glob("*.json")):
        files.append((path, read_json(path)))
    return files


def validate_state(work_dir: Path) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    for path, issue in load_issue_files(work_dir):
        labels = issue.get("labels", [])
        labels = labels if isinstance(labels, list) else []
        if "qa-found" in labels and "codex-ready" in labels:
            required = [
                "change_id",
                "environment",
                "reproduce_steps",
                "actual_result",
                "expected_result",
                "blocking",
            ]
            missing = [field for field in required if field not in issue or issue.get(field) in (None, "", [])]
            evidence = issue.get("evidence")
            if not evidence:
                missing.append("evidence")
            if missing:
                issues.append(
                    ValidationIssue(
                        "error",
                        "issue.evidence.insufficient",
                        f"codex-ready issue is missing required fields: {', '.join(missing)}.",
                        "Use flowspec-qa to supplement the issue before running flowspec-fix.",
                        rel(path, work_dir),
                    )
                )
        if "codex-running" in labels and "fix-proposed" in labels:
            issues.append(
                ValidationIssue(
                    "warning",
                    "issue.state.conflict",
                    "Issue has both codex-running and fix-proposed.",
                    "Remove codex-running after MR evidence is written.",
                    rel(path, work_dir),
                )
            )
    return issues


def matches_any(path: str, patterns: Iterable[str]) -> bool:
    return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)


def validate_guards(work_dir: Path, manifest: dict[str, Any]) -> list[ValidationIssue]:
    issues: list[ValidationIssue] = []
    change_id = manifest.get("change")
    root = worktree_git_root(work_dir)
    if root:
        for path in git_status_paths(root):
            full = root / path
            try:
                relative_to_work = full.resolve().relative_to(work_dir.resolve()).as_posix()
            except ValueError:
                relative_to_work = None
            if relative_to_work is None:
                issues.append(
                    ValidationIssue(
                        "error",
                        "workspace.diff.out_of_scope",
                        "Workspace diff contains files outside the current change workdir.",
                        f"Move or revert out-of-scope files before continuing {change_id}.",
                        path,
                    )
                )

    services_dir = work_dir / "services"
    writable = allowed_services(manifest)
    readonly = readonly_services(manifest)
    for service_path in sorted(services_dir.glob("*")) if services_dir.exists() else []:
        if not service_path.exists():
            continue
        service_name = service_path.name
        changes = git_status_paths(service_path)
        if not changes:
            continue
        if service_name in readonly:
            issues.append(
                ValidationIssue(
                    "human-needed",
                    "service.readonly.modified",
                    f"Read-only service {service_name} has local changes.",
                    "Revert or move changes out of read-only services.",
                    f"services/{service_name}",
                )
            )
        elif service_name not in writable:
            issues.append(
                ValidationIssue(
                    "human-needed",
                    "service.unlisted.modified",
                    f"Service {service_name} has changes but is not writable in manifest.",
                    "Add service through flowspec-change or move the changes.",
                    f"services/{service_name}",
                )
            )
        for changed in changes:
            normalized = changed.replace(os.sep, "/")
            if matches_any(normalized.lower(), HUMAN_GATE_PATTERNS):
                issues.append(
                    ValidationIssue(
                        "human-needed",
                        "automation.human_gate",
                        f"Change in {service_name} may require dependency, DB, config, security, or scope review.",
                        "Ask a human owner to approve before Codex continues.",
                        f"services/{service_name}/{normalized}",
                    )
                )
    return issues


def validate(work_dir: Path) -> dict[str, Any]:
    manifest = read_json(work_dir / "manifest.json")
    change_id = str(manifest.get("change") or work_dir.name)
    issues: list[ValidationIssue] = []
    issues.extend(validate_structure(work_dir, manifest))
    issues.extend(validate_openspec(work_dir, manifest))
    issues.extend(validate_links(work_dir, manifest))
    issues.extend(validate_state(work_dir))
    issues.extend(validate_guards(work_dir, manifest))

    can_continue = not any(issue.severity in BLOCKING_SEVERITIES for issue in issues)
    if any(issue.severity in BLOCKING_SEVERITIES for issue in issues):
        result = "fail"
    elif issues:
        result = "warn"
    else:
        result = "pass"
    return {
        "change_id": change_id,
        "result": result,
        "can_codex_continue": can_continue,
        "issues": [issue.to_dict() for issue in issues],
    }


def print_validation_human(payload: dict[str, Any]) -> None:
    print(f"FlowSpec validate: {payload['change_id']}")
    print(f"Result: {payload['result']}")
    print(f"Can Codex continue: {str(payload['can_codex_continue']).lower()}")
    issues = payload.get("issues", [])
    if not issues:
        print("No issues found.")
        return
    for issue in issues:
        print(f"- [{issue['severity']}] {issue['code']}: {issue['message']}")
        if issue.get("path"):
            print(f"  Path: {issue['path']}")
        if issue.get("fix"):
            print(f"  Fix: {issue['fix']}")


def command_validate(args: argparse.Namespace) -> int:
    work_dir = find_work_dir(Path(args.work_dir or os.getcwd()))
    payload = validate(work_dir)
    if args.json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
    else:
        print_validation_human(payload)
    return 0 if payload["result"] != "fail" else 1


def status_payload(work_dir: Path) -> dict[str, Any]:
    manifest = read_json(work_dir / "manifest.json")
    validation = validate(work_dir)
    return {
        "change_id": manifest.get("change", work_dir.name),
        "title": manifest.get("title", ""),
        "work_dir": str(work_dir),
        "workspace_repo": manifest.get("workspace_repo"),
        "writable_services": sorted(allowed_services(manifest)),
        "readonly_services": sorted(readonly_services(manifest)),
        "main_issue": manifest.get("main_issue"),
        "mrs": manifest.get("mrs", []),
        "validation": {
            "result": validation["result"],
            "can_codex_continue": validation["can_codex_continue"],
            "issue_count": len(validation["issues"]),
        },
    }


def command_status(args: argparse.Namespace) -> int:
    work_dir = find_work_dir(Path(args.work_dir or os.getcwd()))
    payload = status_payload(work_dir)
    if args.json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
        return 0 if payload["validation"]["result"] != "fail" else 1
    print(f"FlowSpec status: {payload['change_id']}")
    print(f"Title: {payload['title']}")
    print(f"Workdir: {work_dir}")
    print(f"Workspace repo: {payload.get('workspace_repo') or 'not set'}")
    print(f"Writable services: {', '.join(payload['writable_services']) or 'none'}")
    print(f"Validation: {payload['validation']['result']} ({payload['validation']['issue_count']} issue(s))")
    print(f"Can Codex continue: {str(payload['validation']['can_codex_continue']).lower()}")
    return 0 if payload["validation"]["result"] != "fail" else 1


def command_init(args: argparse.Namespace) -> int:
    start = Path(args.work_dir or os.getcwd())
    work_dir = find_existing_work_dir(start)
    if work_dir is None:
        work_dir = start.resolve()
        work_dir.mkdir(parents=True, exist_ok=True)
        create_workspace_skeleton(work_dir, args)

    manifest_path = work_dir / "manifest.json"
    manifest = read_json(manifest_path)
    for folder in FLOW_DIRS:
        (work_dir / folder).mkdir(parents=True, exist_ok=True)

    manifest["workspace_path"] = str(work_dir)
    if args.workspace_repo:
        manifest["workspace_repo"] = args.workspace_repo
    else:
        manifest.setdefault("workspace_repo", None)
    if args.main_issue:
        manifest["main_issue"] = args.main_issue
    else:
        manifest.setdefault("main_issue", None)

    service_projects = []
    for service in manifest.get("writable_services", []):
        if not isinstance(service, dict):
            continue
        service_projects.append(
            {
                "name": service.get("name"),
                "target": service.get("target"),
                "work_path": service.get("work_path"),
                "project_url": service.get("project_url"),
            }
        )
    manifest["service_projects"] = service_projects
    manifest.setdefault("mrs", [])
    write_json(manifest_path, manifest)
    if not args.no_codex_skills:
        install_codex_skills(work_dir)
    print(f"Initialized FlowSpec delivery artifacts in {work_dir}")
    return 0


def normalize_change_id(raw: str) -> str:
    change_id = raw.strip().replace(" ", "-")
    if not change_id:
        raise SystemExit("Change id cannot be empty.")
    return change_id


def title_from_change(change_id: str) -> str:
    return change_id.replace("-", " ").replace("_", " ").strip().title() or change_id


def create_workspace_skeleton(work_dir: Path, args: argparse.Namespace) -> None:
    change_id = normalize_change_id(args.change or work_dir.name)
    title = args.title or title_from_change(change_id)
    openspec_change = Path("openspec") / "changes" / change_id
    manifest = {
        "change": change_id,
        "title": title,
        "workspace": args.workspace or work_dir.name,
        "work_dir": args.workspace_path or ".",
        "catalog": args.catalog or "catalog/services.yaml",
        "openspec_change": openspec_change.as_posix(),
        "writable_services": [],
        "readonly_services": [],
    }
    write_json(work_dir / "manifest.json", manifest)

    write_text_if_missing(
        work_dir / "AGENTS.md",
        f"""# FlowSpec Workspace: {change_id}

## Scope

This workspace was initialized by `flowspec init`.

## Writable Services

No writable services are configured yet. Add service symlinks under `services/` and record them in `manifest.json` before asking Codex to edit service code.

## Read-Only Services

No read-only services are configured yet.

## Safety Rules

- Do not copy service repositories into this workspace.
- `services/` entries must be symlinks to real service repositories.
- Keep OpenSpec artifacts under `openspec/changes/{change_id}/`.
- Keep FlowSpec evidence under `flowspec/`.
""",
    )

    write_text_if_missing(
        work_dir / ".gitignore",
        """/services/
/tmp/
/logs/
""",
    )

    change_dir = work_dir / openspec_change
    capability = change_id.replace("_", "-")
    write_text_if_missing(
        change_dir / "proposal.md",
        f"""# {title}

## Why

Initialize a FlowSpec change workspace for `{change_id}`.

## What Changes

- Create the delivery workspace structure.
- Prepare OpenSpec change artifacts.
- Install FlowSpec Codex skills.

## Impact

- No service code changes yet.
""",
    )
    write_text_if_missing(
        change_dir / "design.md",
        f"""# Design

This change starts as an initialized FlowSpec workspace. Add implementation design notes here before service code changes begin.
""",
    )
    write_text_if_missing(
        change_dir / "tasks.md",
        """# Tasks

- [ ] 1.1 Clarify scope and acceptance criteria
- [ ] 1.2 Identify writable and read-only services
- [ ] 1.3 Implement and verify the change
""",
    )
    write_text_if_missing(
        change_dir / "specs" / capability / "spec.md",
        f"""## ADDED Requirements

### Requirement: FlowSpec workspace initialized
The workspace SHALL provide FlowSpec delivery artifacts for `{change_id}`.

#### Scenario: Initialize an empty directory
- **WHEN** `flowspec init` runs in an empty directory
- **THEN** the workspace contains manifest, OpenSpec change artifacts, FlowSpec directories, and Codex skills
""",
    )


def install_codex_skills(work_dir: Path) -> None:
    root = Path(__file__).resolve().parents[1]
    source = root / "skills"
    target = work_dir / ".codex" / "skills"
    target.mkdir(parents=True, exist_ok=True)
    for skill in ["flowspec-change", "flowspec-qa", "flowspec-fix"]:
        src = source / skill
        dst = target / skill
        if dst.exists():
            shutil.rmtree(dst)
        shutil.copytree(src, dst)


def command_archive(args: argparse.Namespace) -> int:
    work_dir = find_work_dir(Path(args.work_dir or os.getcwd()))
    validation = validate(work_dir)
    if not validation["can_codex_continue"]:
        print_validation_human(validation)
        print("Archive blocked by validation.")
        return 1
    manifest = read_json(work_dir / "manifest.json")
    change_id = manifest.get("change", work_dir.name)
    archive_dir = work_dir / "flowspec" / "archive"
    archive_dir.mkdir(parents=True, exist_ok=True)
    archive_file = archive_dir / f"{change_id}.md"
    content = (
        f"# FlowSpec Archive: {change_id}\n\n"
        "## Summary\n\n"
        "Archive summary placeholder. Fill with shipped scope, issue links, MRs, release evidence, and residual risks.\n\n"
        "## Validation\n\n"
        f"- Result: {validation['result']}\n"
        f"- Can Codex continue: {str(validation['can_codex_continue']).lower()}\n"
    )
    if args.dry_run:
        print(content)
        return 0
    archive_file.write_text(content, encoding="utf-8")
    print(f"Wrote {archive_file}")
    return 0


def command_update(args: argparse.Namespace) -> int:
    return run_update()


def command_update_check(args: argparse.Namespace) -> int:
    latest = fetch_latest_version(timeout=5)
    current = current_version()
    payload = {
        "package": PACKAGE_NAME,
        "current": current,
        "latest": latest,
        "update_available": bool(latest and is_newer_version(latest, current)),
        "update_command": "flowspec update",
    }
    if latest:
        write_update_cache(latest)
    if args.json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
    elif latest is None:
        print("Unable to check for updates.")
    elif payload["update_available"]:
        print(f"FlowSpec {latest} is available. You are using {current}.")
        print("Run: flowspec update")
    else:
        print(f"FlowSpec is up to date ({current}).")
    return 0 if latest is not None else 1


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="flowspec")
    sub = parser.add_subparsers(dest="command", required=True)

    status = sub.add_parser("status", help="Show FlowSpec status")
    status.add_argument("--work-dir")
    status.add_argument("--json", action="store_true")
    status.set_defaults(func=command_status)

    init = sub.add_parser("init", help="Initialize or update a FlowSpec change workspace")
    init.add_argument("--work-dir")
    init.add_argument("--change", help="Change id to create when initializing an empty directory")
    init.add_argument("--title", help="Human-readable title used when creating a new manifest")
    init.add_argument("--workspace", help="Workspace name used when creating a new manifest")
    init.add_argument("--workspace-path", help="Workspace-relative path used when creating a new manifest")
    init.add_argument("--catalog", help="Catalog path used when creating a new manifest")
    init.add_argument("--workspace-repo")
    init.add_argument("--main-issue")
    init.add_argument("--no-codex-skills", action="store_true")
    init.set_defaults(func=command_init)

    validate_cmd = sub.add_parser("validate", help="Validate FlowSpec workspace")
    validate_cmd.add_argument("--work-dir")
    validate_cmd.add_argument("--json", action="store_true")
    validate_cmd.set_defaults(func=command_validate)

    archive = sub.add_parser("archive", help="Create archive summary placeholder")
    archive.add_argument("--work-dir")
    archive.add_argument("--dry-run", action="store_true")
    archive.set_defaults(func=command_archive)

    update = sub.add_parser("update", help="Update FlowSpec to the latest version")
    update.set_defaults(func=command_update)

    update_check = sub.add_parser("update-check", help="Check whether a newer FlowSpec version is available")
    update_check.add_argument("--json", action="store_true")
    update_check.set_defaults(func=command_update_check)
    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    maybe_prompt_for_update(args)
    return args.func(args)


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