from __future__ import annotations

import hashlib
import json
import re
import stat
import sys
import zipfile
from datetime import datetime
from pathlib import Path, PurePosixPath
from zoneinfo import ZoneInfo


ROOT = Path(__file__).resolve().parent
META = json.loads((ROOT / "paper-metadata.json").read_text(encoding="utf-8"))
VERSION = META["version"]
ZIP = ROOT / f"SHWRP-2026-030-v{VERSION}.zip"
MANIFEST = ROOT / "release-manifest.json"
CHECKSUM = ROOT / f"{ZIP.name}.sha256"
REPORT = ROOT / "package-validation-report.json"

SENSITIVE_PATH = re.compile(r"(^|[._-])(\.env|password|secret|token|cookie|credential|private[._-]?key)([._-]|$)", re.I)


def now_taipei() -> str:
    return datetime.now(ZoneInfo("Asia/Taipei")).replace(microsecond=0).isoformat()


def sha256_bytes(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def unsafe_name(name: str) -> str | None:
    if not name or "\\" in name or name.startswith("/") or re.match(r"^[A-Za-z]:", name):
        return "absolute, empty, or backslash path"
    path = PurePosixPath(name)
    if any(part in {"", ".", ".."} for part in path.parts):
        return "path traversal or ambiguous path"
    if any(SENSITIVE_PATH.search(part) for part in path.parts):
        return "sensitive-looking path"
    return None


def main() -> None:
    errors: list[str] = []
    checks: dict[str, object] = {}

    if not ZIP.is_file() or not MANIFEST.is_file():
        errors.append("Archive or release-manifest.json is missing")
    else:
        manifest_bytes = MANIFEST.read_bytes()
        try:
            manifest = json.loads(manifest_bytes.decode("utf-8"))
        except (UnicodeDecodeError, json.JSONDecodeError) as error:
            manifest = {}
            errors.append(f"Manifest is not valid UTF-8 JSON: {error}")

        records = manifest.get("files", []) if isinstance(manifest, dict) else []
        record_names = [record.get("path") for record in records if isinstance(record, dict)]
        if len(record_names) != len(records) or any(not isinstance(name, str) for name in record_names):
            errors.append("Manifest file records are malformed")
        if len(set(record_names)) != len(record_names):
            errors.append("Manifest contains duplicate paths")
        for name in record_names:
            issue = unsafe_name(name)
            if issue:
                errors.append(f"Unsafe manifest path {name!r}: {issue}")

        try:
            with zipfile.ZipFile(ZIP) as archive:
                infos = archive.infolist()
                names = [info.filename for info in infos]
                if len(names) != len(set(names)):
                    errors.append("ZIP contains duplicate entry names")
                if len(infos) > 500:
                    errors.append(f"ZIP contains too many entries: {len(infos)}")
                total_expanded = sum(info.file_size for info in infos)
                if total_expanded > 500 * 1024 * 1024:
                    errors.append(f"ZIP expanded size exceeds limit: {total_expanded}")
                for info in infos:
                    issue = unsafe_name(info.filename)
                    if issue:
                        errors.append(f"Unsafe ZIP entry {info.filename!r}: {issue}")
                    if info.flag_bits & 0x1:
                        errors.append(f"Encrypted ZIP entry is not allowed: {info.filename}")
                    mode = (info.external_attr >> 16) & 0xFFFF
                    if stat.S_ISLNK(mode):
                        errors.append(f"Symlink ZIP entry is not allowed: {info.filename}")
                    if info.compress_size and info.file_size / info.compress_size > 200:
                        errors.append(f"ZIP compression ratio exceeds limit: {info.filename}")

                expected_names = sorted([*record_names, "release-manifest.json"])
                if sorted(names) != expected_names:
                    missing = sorted(set(expected_names) - set(names))
                    extra = sorted(set(names) - set(expected_names))
                    errors.append(f"ZIP entry set differs from manifest (missing={missing}, extra={extra})")

                if "release-manifest.json" in names:
                    embedded_manifest = archive.read("release-manifest.json")
                    if embedded_manifest != manifest_bytes:
                        errors.append("Embedded manifest differs from the external manifest")

                records_by_name = {record["path"]: record for record in records if isinstance(record, dict) and isinstance(record.get("path"), str)}
                for name, record in records_by_name.items():
                    if name not in names:
                        continue
                    data = archive.read(name)
                    if record.get("bytes") != len(data):
                        errors.append(f"ZIP byte count mismatch: {name}")
                    if record.get("sha256") != sha256_bytes(data):
                        errors.append(f"ZIP SHA-256 mismatch: {name}")
                    local = ROOT / name
                    if not local.is_file() or local.read_bytes() != data:
                        errors.append(f"ZIP entry differs from the frozen local payload: {name}")

                bad_crc = archive.testzip()
                if bad_crc:
                    errors.append(f"ZIP CRC failure: {bad_crc}")
                checks["entries"] = len(infos)
                checks["expandedBytes"] = total_expanded
        except (zipfile.BadZipFile, OSError) as error:
            errors.append(f"Invalid ZIP archive: {error}")

        if not errors:
            zip_hash = sha256(ZIP)
            CHECKSUM.write_text(f"{zip_hash}  {ZIP.name}\n", encoding="ascii")
            checks.update(
                {
                    "zipBytes": ZIP.stat().st_size,
                    "zipSha256": zip_hash,
                    "manifestSha256": sha256(MANIFEST),
                    "checksumFile": CHECKSUM.name,
                }
            )

    result = {
        "schemaVersion": 2,
        "reportNumber": "SHWRP-2026-030",
        "version": VERSION,
        "validatedAt": now_taipei(),
        "status": "PASSED" if not errors else "FAILED",
        "scope": "Post-package archive integrity and exact-manifest validation; this report and checksum remain outside the immutable ZIP.",
        "checks": checks,
        "errors": errors,
    }
    REPORT.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"status": result["status"], "errors": len(errors), "report": str(REPORT)}, ensure_ascii=False))
    if errors:
        sys.exit(1)


if __name__ == "__main__":
    main()
