from __future__ import annotations

import hashlib
import json
import zipfile
from pathlib import Path


ROOT = Path(__file__).resolve().parent
METADATA_PATH = ROOT / "paper-metadata.json"
METADATA = json.loads(METADATA_PATH.read_text(encoding="utf-8"))
REPORT_NUMBER = str(METADATA["reportNumber"])
VERSION = str(METADATA["version"])
ARCHIVE = ROOT / f"{REPORT_NUMBER}-v{VERSION}.zip"
MANIFEST = ROOT / "release-manifest.json"
HASH_FILE = ROOT / f"{ARCHIVE.name}.sha256"
ROOT_FILES = {
    "README.md", "LICENSE.txt", "RIGHTS-NOTICE.txt", "CITATION.cff", "paper.md", "paper.pdf",
    "paper-metadata.json", "paper-content.json", "source-registry.csv", "source-registry-v1.1-addendum.csv",
    "requirements-traceability-v1.1.csv", "data-management-plan.md",
    "flight-safety-and-consent.md", "pyproject.toml", "requirements.txt", "run_demo.py",
    "run_focused_analysis.py",
    "build_pdf.py", "render_pdf.py", "make_contact_sheets.py", "verify_reproducibility.py",
    "validate_study.py", "package_release.py", "validate_package.py", "update_zenodo_request.py",
    "reproducibility-check.json", "validation-report.json", "pdf-render-report.json",
    "pdf-visual-qa.json",
}
PREFIXES = (
    "m3m_twin/", "tests/", "tests_v1_1/", "config/", "assets/fonts/",
    "outputs/demo/", "outputs/focused/", "outputs/figures/",
)
FORBIDDEN = (
    "__pycache__", ".pyc", ".pytest_cache", "draft-package", "v0.1-draft", "v1.1-draft",
    "paper-metadata-v1.1-draft", "paper-content-v1.1-draft", "zenodo-request.json",
    "zenodo-reserve-receipt", "zenodo-publish-receipt", "precise_latitude", "precise_longitude",
    "raw_html", "full_html", "page_text", "full_description", "review_text", "screenshot",
    "image_binary", "customer_name", "phone", "email", "address",
)


def digest_file(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 selected_files() -> list[Path]:
    result: list[Path] = []
    for path in ROOT.rglob("*"):
        if not path.is_file():
            continue
        relative = path.relative_to(ROOT).as_posix()
        selected = relative in ROOT_FILES or any(relative.startswith(prefix) for prefix in PREFIXES)
        if selected and not any(token.lower() in relative.lower() for token in FORBIDDEN):
            result.append(path)
    return sorted(result, key=lambda item: item.relative_to(ROOT).as_posix())


def main() -> None:
    if VERSION != "1.1" or str(METADATA.get("doi")) != "10.5281/zenodo.21791470":
        raise RuntimeError("release package requires the approved SHWRP-2026-034 v1.1 DOI")
    required = {*ROOT_FILES, "assets/fonts/NotoSansTC-Regular.ttf", "assets/fonts/OFL.txt"}
    missing = sorted(name for name in required if not (ROOT / name).is_file())
    if missing:
        raise FileNotFoundError("release package files missing: " + ", ".join(missing))
    files = selected_files()
    records = [
        {"path": path.relative_to(ROOT).as_posix(), "bytes": path.stat().st_size, "sha256": digest_file(path)}
        for path in files
    ]
    payload = {
        "schemaVersion": 1,
        "createdOn": str(METADATA["dateModified"]),
        "reportNumber": REPORT_NUMBER,
        "version": VERSION,
        "doi": str(METADATA["doi"]),
        "status": "PUBLIC_RELEASE",
        "license": "CC BY 4.0",
        "licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
        "licenseScope": "Author-licensable original analysis, prose, research code and original figures only; third-party content retains its original rights and terms.",
        "publicLicenseGranted": True,
        "fieldFlightExecuted": False,
        "realMavic3MFilesIncluded": False,
        "thirdPartyRawContentIncluded": False,
        "thirdPartyResearchDataIncluded": False,
        "thirdPartyDerivedDataIncluded": True,
        "thirdPartyFontSoftwareIncluded": True,
        "thirdPartyFontLicenseIncluded": True,
        "thirdPartyRedistribution": [
            {
                "path": "assets/fonts/NotoSansTC-Regular.ttf",
                "copyright": "Copyright 2014-2021 Adobe, with Reserved Font Name 'Source'",
                "license": "SIL Open Font License 1.1",
                "licensePath": "assets/fonts/OFL.txt",
                "relicensedUnderCcBy": False,
            }
        ],
        "personalDataIncluded": False,
        "precisePrivateFieldLocationsIncluded": False,
        "syntheticMetricsAreFieldAccuracy": False,
        "fileCount": len(records),
        "files": records,
    }
    MANIFEST.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    if ARCHIVE.exists():
        ARCHIVE.unlink()
    archive_files = [*files, MANIFEST]
    with zipfile.ZipFile(ARCHIVE, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
        for path in sorted(archive_files, key=lambda item: item.relative_to(ROOT).as_posix()):
            relative = path.relative_to(ROOT).as_posix()
            info = zipfile.ZipInfo(relative, date_time=(2026, 8, 4, 0, 0, 0))
            info.compress_type = zipfile.ZIP_DEFLATED
            info.external_attr = 0o644 << 16
            archive.writestr(info, path.read_bytes())
    archive_hash = digest_file(ARCHIVE)
    HASH_FILE.write_text(f"{archive_hash}  {ARCHIVE.name}\n", encoding="utf-8")
    print(json.dumps({"status": "PASSED", "archive": str(ARCHIVE), "bytes": ARCHIVE.stat().st_size, "sha256": archive_hash, "members": len(archive_files)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
