from __future__ import annotations

import csv
import json
import os
import subprocess
import sys
from pathlib import Path

from pypdf import PdfReader


ROOT = Path(__file__).resolve().parent
REPORT = ROOT / "validation-report.json"


def load_json(path: Path) -> dict:
    value = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(value, dict):
        raise TypeError(f"JSON root must be an object: {path}")
    return value


def main() -> None:
    errors: list[str] = []
    warnings: list[str] = []
    checks: dict[str, object] = {}
    required = [
        "paper.md", "paper.pdf", "paper-metadata.json", "paper-content.json", "CITATION.cff",
        "README.md", "LICENSE.txt", "RIGHTS-NOTICE.txt", "source-registry.csv", "dataset-audit.md",
        "study-protocol.md", "data-management-plan.md", "mavic3m-capture-and-qc.md",
        "flight-safety-and-consent.md", "field-label-form.csv", "pdf-render-report.json",
        "pdf-visual-qa.json", "reproducibility-check.json", "outputs/external-blb-sample-audit.json",
        "outputs/demo/engineering-demo-manifest.json", "outputs/demo/artifacts/model.json",
        "outputs/demo/artifacts/model-card.json", "outputs/demo/artifacts/evaluation.json",
        "outputs/demo/artifacts/inference-receipt.json", "outputs/demo/artifacts/split-receipt.json",
        "verify_reproducibility.py", "package_release.py", "validate_package.py", "update_zenodo_request.py",
    ]
    missing = [value for value in required if not (ROOT / value).is_file()]
    checks["requiredFiles"] = {"count": len(required), "missing": missing}
    errors.extend(f"Missing required file: {value}" for value in missing)

    metadata = load_json(ROOT / "paper-metadata.json")
    release_ok = all((
        metadata.get("reportNumber") == "SHWRP-2026-033",
        metadata.get("version") == "1.0",
        metadata.get("datePublished") == "2026-08-04",
        metadata.get("dateModified") == "2026-08-04",
        metadata.get("doi") == "10.5281/zenodo.21787704",
        metadata.get("zenodoUrl") == "https://zenodo.org/records/21787704",
        metadata.get("url") == "https://bento24235111.com/research-journal/m3m-optical-disease-screening",
        metadata.get("license") == "CC BY 4.0",
        metadata.get("peerReviewed") is False,
        metadata.get("fieldDataCollected") is False,
        metadata.get("titleEnglish", "").endswith("a Prospective Field-Validation Framework"),
    ))
    checks["releaseIdentity"] = release_ok
    if not release_ok:
        errors.append("Public v1.0 metadata, DOI, dates, URL, license, or evidence boundary is inconsistent")

    paper = (ROOT / "paper.md").read_text(encoding="utf-8")
    checks["paperHasNoTemplatePlaceholders"] = "{{" not in paper and "}}" not in paper
    if not checks["paperHasNoTemplatePlaceholders"]:
        errors.append("paper.md contains unresolved placeholders")
    for phrase in (
        "合成測試數值不是田間準確率", "RESEARCH_ONLY", "不能確診 BLB",
        "10.5281/zenodo.21787704", "CC BY 4.0", "未經外部同儕審查",
        "第三方權利及授權聲明", "更正申請聲明", "利益衝突", "AI 使用聲明",
        "不存在隸屬、合作、贊助、認可或背書關係",
    ):
        if phrase not in paper:
            errors.append(f"Required claim-boundary phrase is missing from paper.md: {phrase}")

    citation = (ROOT / "CITATION.cff").read_text(encoding="utf-8")
    citation_ok = all(value in citation for value in (
        'version: "1.0"', 'doi: "10.5281/zenodo.21787704"', 'license: "CC-BY-4.0"',
        metadata["url"], metadata["title"],
    ))
    checks["citationMetadata"] = citation_ok
    if not citation_ok:
        errors.append("CITATION.cff is inconsistent with the public v1.0 record")

    license_text = (ROOT / "LICENSE.txt").read_text(encoding="utf-8")
    rights_text = (ROOT / "RIGHTS-NOTICE.txt").read_text(encoding="utf-8")
    rights_ok = all(value in license_text for value in (
        "CC BY 4.0", "10.5281/zenodo.21787704", "author-owned", "not relicense",
    )) and all(value in rights_text for value in ("third-party", "not affiliated", "Corrections"))
    checks["licenseAndThirdPartyRights"] = rights_ok
    if not rights_ok:
        errors.append("License scope or third-party rights boundary is incomplete")

    protocol = (ROOT / "study-protocol.md").read_text(encoding="utf-8")
    sensor_spec_ok = all(value in protocol for value in (
        "four 5 MP multispectral channels", "Green 560 ± 16 nm", "Red 650 ± 16 nm",
        "Red-edge 730 ± 16 nm", "NIR 860 ± 26 nm",
    )) and "5.5 MP" not in protocol and "Green 550 nm" not in protocol
    checks["mavic3mProtocolSensorSpecification"] = sensor_spec_ok
    if not sensor_spec_ok:
        errors.append("study-protocol.md is inconsistent with the cited DJI Mavic 3M sensor specification")

    demo = load_json(ROOT / "outputs" / "demo" / "engineering-demo-manifest.json")
    model_card = load_json(ROOT / "outputs" / "demo" / "artifacts" / "model-card.json")
    evaluation = load_json(ROOT / "outputs" / "demo" / "artifacts" / "evaluation.json")
    inference = load_json(ROOT / "outputs" / "demo" / "artifacts" / "inference-receipt.json")
    split = load_json(ROOT / "outputs" / "demo" / "artifacts" / "split-receipt.json")
    engineering_gate = all([
        demo.get("status") == "PASSED",
        demo.get("dataProvenance") == "synthetic",
        demo.get("fieldPerformanceEstablished") is False,
        demo.get("modelPromotionStatus") == "RESEARCH_ONLY",
        model_card.get("promotionStatus") == "RESEARCH_ONLY",
        model_card.get("fieldPerformanceEstablished") is False,
        evaluation.get("engineeringSyntheticOnly") is True,
        split.get("leakageDetected") is False,
        inference.get("automationAuthorized") is False,
        inference.get("diseaseConfirmed") is False,
        inference.get("treatmentRecommendationPresent") is False,
        inference.get("status") in {"REJECTED_QC", "REJECTED_OOD", "NO_FLAG", "REVIEW_REQUIRED"},
    ])
    checks["engineeringClaimBoundary"] = engineering_gate
    if not engineering_gate:
        errors.append("Engineering outputs violate the synthetic-only or fail-closed claim boundary")

    external = load_json(ROOT / "outputs" / "external-blb-sample-audit.json")
    external_ok = external.get("status") == "PASSED" and external.get("source", {}).get("redistributedByThisPackage") is False
    checks["externalDatasetAudit"] = external_ok
    if not external_ok:
        errors.append("External BLB sample audit did not pass or indicates redistribution")

    with (ROOT / "source-registry.csv").open("r", encoding="utf-8-sig", newline="") as stream:
        sources = list(csv.DictReader(stream))
    checks["sourceRegistry"] = {"rows": len(sources), "allHaveUrl": all(row.get("url") for row in sources)}
    if len(sources) < 10 or not all(row.get("url") for row in sources):
        errors.append("Source registry is incomplete")

    third_party_patterns = ("BLB_prj_source_code.ipynb", "D1-test-image_patch", "D1-test-label_patch", "GD-RGBMS-DB.zip")
    exposed = [str(path.relative_to(ROOT)) for path in ROOT.rglob("*") if path.is_file() and any(token in path.name for token in third_party_patterns)]
    checks["thirdPartyRawFilesExcluded"] = {"passed": not exposed, "exposed": exposed}
    if exposed:
        errors.extend(f"Third-party raw inspection file is inside the study package: {value}" for value in exposed)

    test = subprocess.run(
        [sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], cwd=ROOT,
        capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=180, check=False,
        env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
    )
    progress_line = next((line for line in test.stdout.splitlines() if "[100%]" in line), "")
    tests_ok = test.returncode == 0 and progress_line.count(".") == 33
    checks["automatedTests"] = {"passed": tests_ok, "summary": test.stdout.strip().splitlines()[-1] if test.stdout.strip() else ""}
    if not tests_ok:
        errors.append("Automated test suite did not report 33 passed")

    reader = PdfReader(str(ROOT / "paper.pdf"))
    pdf_text = "\n".join(page.extract_text() or "" for page in reader.pages)
    pdf_ok = len(reader.pages) >= 10 and all(value in pdf_text for value in (
        "從多光譜影像到可複查病害圖", "RESEARCH_ONLY", "參考文獻與官方資料",
        "10.5281/zenodo.21787704", "CC BY 4.0", "未經外部同儕審查",
        "不存在隸屬、合作、贊助、認可或背書關係",
    ))
    checks["pdf"] = {"passed": pdf_ok, "pages": len(reader.pages), "textCharacters": len(pdf_text)}
    if not pdf_ok:
        errors.append("PDF page count or searchable-text checks failed")
    render = load_json(ROOT / "pdf-render-report.json")
    visual = load_json(ROOT / "pdf-visual-qa.json")
    if render.get("status") != "PASSED" or visual.get("status") != "PASSED":
        errors.append("PDF render or visual QA report did not pass")

    reproducibility = load_json(ROOT / "reproducibility-check.json")
    if reproducibility.get("status") != "PASSED" or len(reproducibility.get("identicalArtifacts", {})) != 5:
        errors.append("Isolated reproducibility comparison did not pass")

    pycache = [str(path.relative_to(ROOT)) for path in ROOT.rglob("__pycache__") if path.is_dir()]
    if pycache:
        warnings.append("Local Python caches exist and must remain outside any release allowlist: " + ", ".join(pycache))
    checks["localCacheWarning"] = pycache

    report = {
        "schemaVersion": 1,
        "validatedAt": "2026-08-04",
        "status": "PASSED" if not errors else "FAILED",
        "scope": "Public v1.0 identity, DOI, license and third-party-rights boundary, software, external-data exclusion, 33-test suite, searchable PDF, render, and visual-QA checks.",
        "checks": checks,
        "errors": errors,
        "warnings": warnings,
    }
    REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"status": report["status"], "errors": len(errors), "warnings": len(warnings)}, ensure_ascii=False))
    if errors:
        raise SystemExit(1)


if __name__ == "__main__":
    main()
