#!/usr/bin/env python3
"""Generate CI pipeline YAML from detected stack data.

Input sources:
- --input stack report JSON file
- stdin stack report JSON
- --repo path (auto-detect stack)

Output:
- text/json summary
- pipeline YAML written via --output or printed to stdout
"""

import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional


class CLIError(Exception):
    """Raised for expected CLI failures."""


@dataclass
class PipelineSummary:
    platform: str
    output: str
    stages: List[str]
    uses_cache: bool
    languages: List[str]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate CI/CD pipeline YAML from detected stack.")
    parser.add_argument("--input", help="Stack report JSON file. If omitted, can read stdin JSON.")
    parser.add_argument("--repo", help="Repository path for auto-detection fallback.")
    parser.add_argument("--platform", choices=["github", "gitlab"], required=True, help="Target CI platform.")
    parser.add_argument("--output", help="Write YAML to this file; otherwise print to stdout.")
    parser.add_argument("--format", choices=["text", "json"], default="text", help="Summary output format.")
    return parser.parse_args()


def load_json_input(input_path: Optional[str]) -> Optional[Dict[str, Any]]:
    if input_path:
        try:
            return json.loads(Path(input_path).read_text(encoding="utf-8"))
        except Exception as exc:
            raise CLIError(f"Failed reading --input: {exc}") from exc

    if not sys.stdin.isatty():
        raw = sys.stdin.read().strip()
        if raw:
            try:
                return json.loads(raw)
            except json.JSONDecodeError as exc:
                raise CLIError(f"Invalid JSON from stdin: {exc}") from exc

    return None


def detect_stack(repo: Path) -> Dict[str, Any]:
    scripts = {}
    pkg_file = repo / "package.json"
    if pkg_file.exists():
        try:
            pkg = json.loads(pkg_file.read_text(encoding="utf-8"))
            raw_scripts = pkg.get("scripts", {})
            if isinstance(raw_scripts, dict):
                scripts = raw_scripts
        except Exception:
            scripts = {}

    languages: List[str] = []
    if pkg_file.exists():
        languages.append("node")
    if (repo / "pyproject.toml").exists() or (repo / "requirements.txt").exists():
        languages.append("python")
    if (repo / "go.mod").exists():
        languages.append("go")

    return {
        "languages": sorted(set(languages)),
        "signals": {
            "pnpm_lock": (repo / "pnpm-lock.yaml").exists(),
            "yarn_lock": (repo / "yarn.lock").exists(),
            "npm_lock": (repo / "package-lock.json").exists(),
            "dockerfile": (repo / "Dockerfile").exists(),
        },
        "lint_commands": ["npm run lint"] if "lint" in scripts else [],
        "test_commands": ["npm test"] if "test" in scripts else [],
        "build_commands": ["npm run build"] if "build" in scripts else [],
    }


def select_node_install(signals: Dict[str, Any]) -> str:
    if signals.get("pnpm_lock"):
        return "pnpm install --frozen-lockfile"
    if signals.get("yarn_lock"):
        return "yarn install --frozen-lockfile"
    return "npm ci"


def github_yaml(stack: Dict[str, Any]) -> str:
    langs = stack.get("languages", [])
    signals = stack.get("signals", {})
    lint_cmds = stack.get("lint_commands", []) or ["echo 'No lint command configured'"]
    test_cmds = stack.get("test_commands", []) or ["echo 'No test command configured'"]
    build_cmds = stack.get("build_commands", []) or ["echo 'No build command configured'"]

    lines: List[str] = [
        "name: CI",
        "on:",
        "  push:",
        "    branches: [main, develop]",
        "  pull_request:",
        "    branches: [main, develop]",
        "",
        "jobs:",
    ]

    if "node" in langs:
        lines.extend(
            [
                "  node-ci:",
                "    runs-on: ubuntu-latest",
                "    steps:",
                "      - uses: actions/checkout@v4",
                "      - uses: actions/setup-node@v4",
                "        with:",
                "          node-version: '20'",
                "          cache: 'npm'",
                f"      - run: {select_node_install(signals)}",
            ]
        )
        for cmd in lint_cmds + test_cmds + build_cmds:
            lines.append(f"      - run: {cmd}")

    if "python" in langs:
        lines.extend(
            [
                "  python-ci:",
                "    runs-on: ubuntu-latest",
                "    steps:",
                "      - uses: actions/checkout@v4",
                "      - uses: actions/setup-python@v5",
                "        with:",
                "          python-version: '3.12'",
                "      - run: python3 -m pip install -U pip",
                "      - run: python3 -m pip install -r requirements.txt || true",
                "      - run: python3 -m pytest || true",
            ]
        )

    if "go" in langs:
        lines.extend(
            [
                "  go-ci:",
                "    runs-on: ubuntu-latest",
                "    steps:",
                "      - uses: actions/checkout@v4",
                "      - uses: actions/setup-go@v5",
                "        with:",
                "          go-version: '1.22'",
                "      - run: go test ./...",
                "      - run: go build ./...",
            ]
        )

    return "\n".join(lines) + "\n"


def gitlab_yaml(stack: Dict[str, Any]) -> str:
    langs = stack.get("languages", [])
    signals = stack.get("signals", {})
    lint_cmds = stack.get("lint_commands", []) or ["echo 'No lint command configured'"]
    test_cmds = stack.get("test_commands", []) or ["echo 'No test command configured'"]
    build_cmds = stack.get("build_commands", []) or ["echo 'No build command configured'"]

    lines: List[str] = [
        "stages:",
        "  - lint",
        "  - test",
        "  - build",
        "",
    ]

    if "node" in langs:
        install_cmd = select_node_install(signals)
        lines.extend(
            [
                "node_lint:",
                "  image: node:20",
                "  stage: lint",
                "  script:",
                f"    - {install_cmd}",
            ]
        )
        for cmd in lint_cmds:
            lines.append(f"    - {cmd}")
        lines.extend(
            [
                "",
                "node_test:",
                "  image: node:20",
                "  stage: test",
                "  script:",
                f"    - {install_cmd}",
            ]
        )
        for cmd in test_cmds:
            lines.append(f"    - {cmd}")
        lines.extend(
            [
                "",
                "node_build:",
                "  image: node:20",
                "  stage: build",
                "  script:",
                f"    - {install_cmd}",
            ]
        )
        for cmd in build_cmds:
            lines.append(f"    - {cmd}")

    if "python" in langs:
        lines.extend(
            [
                "",
                "python_test:",
                "  image: python:3.12",
                "  stage: test",
                "  script:",
                "    - python3 -m pip install -U pip",
                "    - python3 -m pip install -r requirements.txt || true",
                "    - python3 -m pytest || true",
            ]
        )

    if "go" in langs:
        lines.extend(
            [
                "",
                "go_test:",
                "  image: golang:1.22",
                "  stage: test",
                "  script:",
                "    - go test ./...",
                "    - go build ./...",
            ]
        )

    return "\n".join(lines) + "\n"


def main() -> int:
    args = parse_args()
    stack = load_json_input(args.input)

    if stack is None:
        if not args.repo:
            raise CLIError("Provide stack input via --input/stdin or set --repo for auto-detection.")
        repo = Path(args.repo).resolve()
        if not repo.exists() or not repo.is_dir():
            raise CLIError(f"Invalid repo path: {repo}")
        stack = detect_stack(repo)

    if args.platform == "github":
        yaml_content = github_yaml(stack)
    else:
        yaml_content = gitlab_yaml(stack)

    output_path = args.output or "stdout"
    if args.output:
        out = Path(args.output)
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(yaml_content, encoding="utf-8")
    else:
        print(yaml_content, end="")

    summary = PipelineSummary(
        platform=args.platform,
        output=output_path,
        stages=["lint", "test", "build"],
        uses_cache=True,
        languages=stack.get("languages", []),
    )

    if args.format == "json":
        print(json.dumps(asdict(summary), indent=2), file=sys.stderr if not args.output else sys.stdout)
    else:
        text = (
            "Pipeline generated\n"
            f"- platform: {summary.platform}\n"
            f"- output: {summary.output}\n"
            f"- stages: {', '.join(summary.stages)}\n"
            f"- languages: {', '.join(summary.languages) if summary.languages else 'none'}"
        )
        print(text, file=sys.stderr if not args.output else sys.stdout)

    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except CLIError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        raise SystemExit(2)
