"""Audit the exercise package for staleness and broken cross-references.

Checks every filename named in the draft, the cover note and the deployment
guide against what is actually on disk, then reports what is missing, what is
orphaned and what is stale.
"""
import glob
import re
import zipfile
from datetime import datetime
from pathlib import Path

ROOT = Path(r"D:\GIS Center\Publications\GeoAI\Exercises")
SITE = ROOT / "site" / "_site"

DOCS = (sorted(ROOT.glob("GeoAIRisks_Exercises_*.md"))
        + sorted(ROOT.glob("README_Exercise_Package_*.md"))
        + sorted((ROOT / "site").glob("Deployment_Guide*.md")))

# Any token that looks like a filename.
FNAME = re.compile(r"[A-Za-z0-9_\-]+\.(?:py|csv|json|png|docx|geojson|zip|"
                   r"css|html|txt|dat|md)\b")

SEARCH_DIRS = ["", "figures", "handoff", "snapshots", "toolkit",
               "lab_artifacts", "site", "site/_site"]


def on_disk():
    names = {}
    for p in ROOT.rglob("*"):
        if p.is_file():
            names.setdefault(p.name, []).append(
                p.relative_to(ROOT).as_posix())
    return names


def main():
    disk = on_disk()
    print("=" * 72)
    print("CROSS-REFERENCE AUDIT")
    print("=" * 72)

    referenced = {}
    for d in DOCS:
        for m in FNAME.finditer(d.read_text(encoding="utf-8")):
            referenced.setdefault(m.group(0), set()).add(d.name)

    missing = {n: s for n, s in referenced.items() if n not in disk}
    print(f"\nfilenames referenced in the documents : {len(referenced)}")
    print(f"of those, absent from disk            : {len(missing)}")
    for n, s in sorted(missing.items()):
        print(f"   MISSING  {n:<42} named in {', '.join(sorted(s))}")

    # Figures referenced by the draft as images.
    draft = DOCS[0].read_text(encoding="utf-8")
    imgs = set(re.findall(r"!\[[^\]]*\]\(figures/([^)]+)\)", draft))
    have = {p.name for p in (ROOT / "figures").glob("*.png")}
    print(f"\nfigures placed inline in the draft    : {len(imgs)}")
    print(f"figures present in figures/           : {len(have)}")
    for n in sorted(imgs - have):
        print(f"   MISSING FIGURE  {n}")
    for n in sorted(have - imgs):
        print(f"   ORPHAN FIGURE   {n}  (on disk, not placed in the draft)")

    # Staleness: anything in the site older than its source.
    print("\nSTALENESS")
    # Each document in the site tree answers to its own source, since the
    # package now carries the fused draft, two generated editions and the
    # README, each stamped when its own build ran.
    def newest(prefix):
        found = sorted(ROOT.glob(prefix + "*.docx"))
        return found[-1].name if found else None

    PREFIXES = ("GeoAIRisks_Exercises_", "GeoAIRisks_Labs_Instructor_Edition_",
                "GeoAIRisks_Labs_Student_Edition_", "README_Exercise_Package_")
    src_docx = sorted(ROOT.glob("GeoAIRisks_Exercises_*.docx"))[-1]
    print(f"   workbook on disk : {src_docx.name}")
    site_docx = (list((SITE / "instructor" / "files").glob("*.docx"))
                 + list((SITE / "files" / "documents").glob("*.docx")))
    for f in site_docx:
        pre = next((p for p in PREFIXES if f.name.startswith(p)), None)
        current = newest(pre) if pre else None
        flag = "OK" if current == f.name else "STALE"
        print(f"   in site tree     : {f.name}  [{flag}]")

    for pat in ("geoai_site_public_*.zip", "geoai_site_instructor_*.zip"):
        found = sorted((ROOT / "site").glob(pat))
        if not found:
            print(f"   upload zip       : none matching {pat}  [MISSING]")
            continue
        zp = found[-1]
        with zipfile.ZipFile(zp) as z:
            names = z.namelist()
        newest_site = max(p.stat().st_mtime for p in SITE.rglob("*")
                          if p.is_file())
        flag = "OK" if zp.stat().st_mtime >= newest_site else "STALE"
        print(f"   {zp.name:<44} {len(names)} entries, "
              f"{zp.stat().st_size/1e6:.1f} MB  [{flag}]")
        for n in sorted(n for n in names if n.endswith(".docx")):
            print(f"      contains {n}")
        stray = [n for n in names if n.startswith("instructor/")]             if "public" in zp.name else []
        if stray:
            print(f"      LEAK: {len(stray)} instructor files in the public "
                  f"package")

    # Junk.
    print("\nJUNK AND STRAYS")
    junk = []
    for pat in ("**/__pycache__", "**/*.pyc", "**/.bak*", "**/*.bak",
                "**/_shp", "**/*.log", "**/*.tmp"):
        junk += [p for p in ROOT.glob(pat)]
    stray_zip = [p for p in ROOT.glob("*.zip")]
    for p in junk:
        print(f"   JUNK   {p.relative_to(ROOT).as_posix()}")
    for p in stray_zip:
        age = datetime.fromtimestamp(p.stat().st_mtime).strftime("%m-%d %H:%M")
        print(f"   ZIP AT TOP LEVEL  {p.name}  {p.stat().st_size/1e6:.1f} MB  "
              f"{age}")
    if not junk and not stray_zip:
        print("   none")

    print("\nTOTALS")
    n_files = sum(1 for p in ROOT.rglob("*") if p.is_file())
    size = sum(p.stat().st_size for p in ROOT.rglob("*") if p.is_file())
    print(f"   {n_files} files, {size/1e6:.1f} MB")


if __name__ == "__main__":
    main()
