#!/usr/bin/env python3
"""Verify that declared shots, inline markers and PNG files all agree.

Usage:
  check-shots.py <guides-draft-dir>          # e.g. /srv/apps/<proj>/docs/guides-draft
  check-shots.py <dir> --quiet               # exit code only

Expects that directory to contain:
  guides/*.html      guide bodies with inline <!-- BWG-SHOT id="..." ... --> markers
  _frag/*.shots.md   per-guide shot declarations ("- **id:** <shot-id>" lines)
  shots/*.png        the captured images

WHY THIS EXISTS
---------------
Four ways for these three to drift, all of them SILENT — nothing errors, and the
guide simply publishes without an image (or with one nobody planned):

  1. A declared shot with no inline marker. The image gets captured, gets
     imported to the media library, and never appears in the guide. This
     actually happened to 6 images across 4 guides: they existed, they were
     imported, and every reader saw a guide with no screenshot. It was visible
     in an import report as "orphan PNG, no marker in any draft" and got
     triaged as housekeeping rather than as the defect it was.
  2. Two markers for one shot id — the same image embedded twice.
  3. A marker whose PNG isn't on disk. The embedder strips it (correct, so
     nothing internal leaks) but the guide silently loses a planned image.
  4. A PNG with no marker anywhere — the same failure as (1), seen from the
     other side, and the one that catches shots nobody remembered to place.

A shot deliberately not captured must say so in its fragment, with
"NOT CAPTURED" on the id line — that marks the omission as a decision rather
than an oversight, and this check then expects it to have no marker.
"""
import glob
import os
import re
import sys
from collections import Counter

MARKER = re.compile(r'<!--\s*BWG-SHOT\b(.*?)-->', re.S)
ATTR = re.compile(r'(\w+)\s*=\s*"([^"]*)"')
FRAG_ID = re.compile(r'^\s*-\s*\*\*id:\*\*\s*([A-Za-z0-9._-]+)(.*)$', re.M)


def main():
    if len(sys.argv) < 2:
        sys.exit(__doc__)
    root = sys.argv[1].rstrip('/')
    quiet = '--quiet' in sys.argv

    # Inline markers, per guide.
    markers = Counter()
    marker_where = {}
    for path in sorted(glob.glob(f'{root}/guides/*.html')):
        body = open(path).read()
        for m in MARKER.finditer(body):
            sid = dict(ATTR.findall(m.group(1))).get('id', '')
            if sid:
                markers[sid] += 1
                marker_where.setdefault(sid, []).append(os.path.basename(path))

    # Declared shots, and which are deliberately uncaptured.
    declared, not_captured = set(), set()
    for path in sorted(glob.glob(f'{root}/_frag/*.shots.md')):
        for sid, rest in FRAG_ID.findall(open(path).read()):
            declared.add(sid)
            if 'NOT CAPTURED' in rest.upper():
                not_captured.add(sid)

    # PNGs on disk.
    pngs = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f'{root}/shots/*.png')}

    problems = []

    for sid in sorted(declared - not_captured):
        if markers[sid] == 0:
            problems.append(f'DECLARED BUT NEVER PLACED: {sid} — has a fragment entry (and likely a '
                            f'captured image) but no inline marker, so no guide will ever show it')

    for sid in sorted(not_captured):
        if markers[sid]:
            problems.append(f'MARKER FOR AN UNCAPTURED SHOT: {sid} — fragment says NOT CAPTURED but '
                            f'{", ".join(marker_where[sid])} still has a marker; it will be silently stripped')

    for sid, n in sorted(markers.items()):
        if n > 1:
            problems.append(f'DUPLICATE MARKER: {sid} appears {n}× ({", ".join(marker_where[sid])})')
        if sid not in pngs:
            problems.append(f'MARKER WITH NO IMAGE: {sid} in {", ".join(marker_where[sid])} — no '
                            f'shots/{sid}.png, so the marker will be stripped and the guide loses it')

    for sid in sorted(pngs - set(markers)):
        problems.append(f'IMAGE NEVER DISPLAYED: shots/{sid}.png exists but no guide has a marker '
                        f'for it — captured, probably imported, invisible to every reader')

    if not quiet:
        print(f'guides={len(glob.glob(f"{root}/guides/*.html"))} '
              f'declared={len(declared)} (not-captured={len(not_captured)}) '
              f'markers={sum(markers.values())} pngs={len(pngs)}')
        if problems:
            print(f'\n{len(problems)} problem(s):')
            for p in problems:
                print(f'  · {p}')
        else:
            print('OK — declarations, markers and images all agree.')

    sys.exit(1 if problems else 0)


if __name__ == '__main__':
    main()
