#!/usr/bin/env python3
"""Regenerate the deploy manifest and CycloneDX file inventory from actual app files."""
from __future__ import annotations

import fnmatch
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "app"
MANIFEST = APP / "DEPLOYMENT_MANIFEST.json"
SBOM = APP / "sbom.cdx.json"
VERSION = (APP / "VERSION").read_text(encoding="utf-8").strip()


def is_changes(path: Path) -> bool:
    return fnmatch.fnmatch(path.name, "CHANGES_*.txt")


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def files_excluding(*names: str) -> list[Path]:
    excluded = set(names)
    return sorted(
        path
        for path in APP.rglob("*")
        if path.is_file() and path.name not in excluded and not is_changes(path)
    )


old_sbom = json.loads(SBOM.read_text(encoding="utf-8"))
libraries = [component for component in old_sbom.get("components", []) if component.get("type") == "library"]
for component in libraries:
    properties = {item.get("name"): item.get("value") for item in component.get("properties", [])}
    deployment_path = properties.get("glmchat.deployment_path")
    if deployment_path and (APP / deployment_path).is_file():
        component["hashes"] = [{"alg": "SHA-256", "content": sha256(APP / deployment_path)}]
    for item in component.get("properties", []):
        if item.get("name") == "glmchat.review_status":
            item["value"] = "legacy dependency retained; upgrade requires controlled compatibility work and full browser regression"

sbom_files = files_excluding("DEPLOYMENT_MANIFEST.json", "sbom.cdx.json")
file_components = []
for path in sbom_files:
    relative = path.relative_to(APP).as_posix()
    file_components.append(
        {
            "type": "file",
            "name": relative,
            "version": VERSION,
            "hashes": [{"alg": "SHA-256", "content": sha256(path)}],
            "properties": [{"name": "glmchat.deployment_path", "value": relative}],
        }
    )

sbom = {
    "bomFormat": "CycloneDX",
    "specVersion": old_sbom.get("specVersion", "1.5"),
    "serialNumber": old_sbom.get("serialNumber", "urn:uuid:50ae43f6-b7cf-4cd7-9d5c-84d6575fab4a"),
    "version": int(old_sbom.get("version", 1)),
    "metadata": {
        "timestamp": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
        "component": {"type": "application", "name": "GLMChat", "version": VERSION},
        "properties": [
            {"name": "glmchat.release_profile", "value": "server-direct-deploy"},
            {
                "name": "glmchat.file_inventory_scope",
                "value": "all server-direct-deploy files except self-referential SBOM, deployment manifest and timestamped CHANGES reports",
            },
            {
                "name": "glmchat.production_acceptance",
                "value": "pending live LiteSpeed, Together provider and physical Android checks",
            },
        ],
    },
    "components": file_components + libraries,
}
SBOM.write_text(json.dumps(sbom, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

manifest_files = files_excluding("DEPLOYMENT_MANIFEST.json")
entries = {}
for path in manifest_files:
    relative = path.relative_to(APP).as_posix()
    entries[relative] = {"sha256": sha256(path), "size": path.stat().st_size}
manifest = {
    "application": "GLMChat",
    "file_count": len(entries),
    "files": entries,
    "inventory_exclusions": ["DEPLOYMENT_MANIFEST.json", "CHANGES_hhmmddmmyyyy.txt"],
    "profile": "server-direct-deploy",
    "version": VERSION,
}
MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"Refreshed SBOM ({len(file_components)} files, {len(libraries)} libraries) and deployment manifest ({len(entries)} files).")
