#!/usr/bin/env python3
"""Repository-local supply-chain checks with no third-party runtime dependency."""

from __future__ import annotations

import argparse
import base64
import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Iterable
from urllib.parse import quote


SECRET_PATTERNS = {
    "private-key": re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----"),
    "jwt": re.compile(rb"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
    "aws-access-key": re.compile(rb"\b(?:AKIA|ASIA)[A-Z0-9]{16}\b"),
    "github-token": re.compile(rb"\b(?:gh[pousr]_[A-Za-z0-9]{36,255}|github_pat_[A-Za-z0-9_]{82,255})\b"),
    "google-api-key": re.compile(rb"\bAIza[A-Za-z0-9_-]{35}\b"),
    "slack-token": re.compile(rb"\bxox[baprs]-[A-Za-z0-9-]{20,}\b"),
    "stripe-live-key": re.compile(rb"\bsk_live_[A-Za-z0-9]{16,}\b"),
}

MAX_SCANNED_FILE_BYTES = 2 * 1024 * 1024


def tracked_files(root: Path) -> list[str]:
    result = subprocess.run(
        ["git", "ls-files", "-z"],
        cwd=root,
        check=True,
        capture_output=True,
    )
    return [item.decode("utf-8", errors="surrogateescape") for item in result.stdout.split(b"\0") if item]


def scan_files(root: Path, paths: Iterable[str]) -> list[tuple[str, str, int]]:
    findings: list[tuple[str, str, int]] = []
    for relative in paths:
        path = root / relative
        if not path.is_file() or path.stat().st_size > MAX_SCANNED_FILE_BYTES:
            continue
        data = path.read_bytes()
        if b"\0" in data:
            continue
        for line_number, line in enumerate(data.splitlines(), start=1):
            for secret_type, pattern in SECRET_PATTERNS.items():
                if pattern.search(line):
                    findings.append((secret_type, relative.replace("\\", "/"), line_number))
    return findings


def _purl(ecosystem: str, name: str, version: str) -> str:
    if ecosystem == "npm" and name.startswith("@"):
        namespace, package = name[1:].split("/", 1)
        return f"pkg:npm/{quote(namespace, safe='')}/{quote(package, safe='')}@{quote(version, safe='')}"
    return f"pkg:{ecosystem}/{quote(name, safe='')}@{quote(version, safe='')}"


def _component(ecosystem: str, name: str, version: str, manifests: set[str], scope: str = "required", hashes: list[dict[str, str]] | None = None) -> dict[str, object]:
    component: dict[str, object] = {
        "type": "library",
        "bom-ref": _purl(ecosystem, name, version),
        "name": name,
        "version": version,
        "scope": scope,
        "purl": _purl(ecosystem, name, version),
        "properties": [{"name": "jobjakt:source-manifest", "value": manifest} for manifest in sorted(manifests)],
    }
    if hashes:
        component["hashes"] = hashes
    return component


def _npm_components(root: Path) -> list[dict[str, object]]:
    relative = "job-tracker-ui/package-lock.json"
    lock = json.loads((root / relative).read_text(encoding="utf-8"))
    components: dict[tuple[str, str], dict[str, object]] = {}
    for package_path, package in lock.get("packages", {}).items():
        if not package_path or not package.get("version"):
            continue
        name = package.get("name") or package_path.rsplit("node_modules/", 1)[-1]
        version = str(package["version"])
        integrity = str(package.get("integrity", ""))
        hashes: list[dict[str, str]] = []
        if integrity.startswith("sha512-"):
            try:
                hashes.append({"alg": "SHA-512", "content": base64.b64decode(integrity[7:]).hex()})
            except ValueError:
                pass
        components[(name, version)] = _component(
            "npm",
            name,
            version,
            {relative},
            "optional" if package.get("dev") else "required",
            hashes,
        )
    return list(components.values())


def _nuget_components(root: Path) -> list[dict[str, object]]:
    manifests = ["JobTrackerApi/packages.lock.json", "JobTrackerApi.Tests/packages.lock.json"]
    packages: dict[tuple[str, str], set[str]] = {}
    hashes: dict[tuple[str, str], str] = {}
    for relative in manifests:
        lock = json.loads((root / relative).read_text(encoding="utf-8"))
        for target in lock.get("dependencies", {}).values():
            for name, package in target.items():
                version = str(package.get("resolved", ""))
                if not version:
                    continue
                key = (name, version)
                packages.setdefault(key, set()).add(relative)
                if package.get("contentHash"):
                    hashes[key] = str(package["contentHash"])
    result = []
    for (name, version), sources in packages.items():
        package_hashes = []
        if (content_hash := hashes.get((name, version))):
            try:
                package_hashes.append({"alg": "SHA-512", "content": base64.b64decode(content_hash).hex()})
            except ValueError:
                pass
        result.append(_component("nuget", name, version, sources, hashes=package_hashes))
    return result


def _python_components(root: Path) -> list[dict[str, object]]:
    relative = "tools/summarizer/requirements-linux.lock"
    pattern = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s\\]+)")
    packages: dict[tuple[str, str], set[str]] = {}
    for line in (root / relative).read_text(encoding="utf-8").splitlines():
        if match := pattern.match(line):
            packages.setdefault((match.group(1), match.group(2)), set()).add(relative)
    return [_component("pypi", name, version, sources) for (name, version), sources in packages.items()]


def generate_sbom(root: Path) -> dict[str, object]:
    lock_paths = [
        "job-tracker-ui/package-lock.json",
        "JobTrackerApi/packages.lock.json",
        "JobTrackerApi.Tests/packages.lock.json",
        "tools/summarizer/requirements-linux.lock",
    ]
    digest = hashlib.sha256()
    for relative in lock_paths:
        digest.update(relative.encode("utf-8"))
        digest.update((root / relative).read_bytes())

    components = _npm_components(root) + _nuget_components(root) + _python_components(root)
    components.sort(key=lambda item: (str(item["purl"]).lower(), str(item["version"])))
    return {
        "bomFormat": "CycloneDX",
        "specVersion": "1.5",
        "serialNumber": f"urn:uuid:{digest.hexdigest()[:8]}-{digest.hexdigest()[8:12]}-5{digest.hexdigest()[13:16]}-a{digest.hexdigest()[17:20]}-{digest.hexdigest()[20:32]}",
        "version": 1,
        "metadata": {
            "component": {"type": "application", "bom-ref": "pkg:generic/jobjakt", "name": "Jobjakt"},
            "tools": {"components": [{"type": "application", "name": "scripts/supply-chain.py"}]},
            "properties": [{"name": "jobjakt:lock-digest-sha256", "value": digest.hexdigest()}],
        },
        "components": components,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("command", choices=("scan-secrets", "generate-sbom"))
    parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()
    root = args.root.resolve()

    if args.command == "scan-secrets":
        findings = scan_files(root, tracked_files(root))
        for secret_type, relative, line_number in findings:
            print(f"{secret_type}: {relative}:{line_number}")
        if findings:
            print(f"Tracked-secret scan failed with {len(findings)} high-confidence finding(s); values were suppressed.", file=sys.stderr)
            return 1
        print("Tracked-secret scan passed; no high-confidence credential patterns found.")
        return 0

    if not args.output:
        parser.error("generate-sbom requires --output")
    output = args.output if args.output.is_absolute() else root / args.output
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(json.dumps(generate_sbom(root), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    print(f"CycloneDX SBOM written to {output}.")
    return 0


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