#!/usr/bin/env python3
"""Build phase for the Brentwood 100 CPT migration.

Source: notes/brentwood-100-db-dump.json (Adi's full extract of the 149 content
elements of page 157 / published version 10175 — flags, header, number, body,
photos with size variants). No DB access needed; fully reproducible from the dump.

The 87 mosaic tiles are exactly the elements with expandable=null & randomize=1.
Each tile is classified:
  - MODAL   : its cover photo has a `link` (#c-<uuid>) -> opens a story. The story
              is the immediately following element (expandable=modal); verified
              58/58 by both link-uuid and adjacency. Title/body/number/detail-gallery
              come from the STORY; cover image + caption come from the TILE.
  - INLINE  : no cover link, has body -> self-contained story shown in the grid.
  - MONTAGE : no cover link, no body -> photo tile with a caption only.

Unlisted elements (Country Roads + a hidden tile) are emitted as `draft`.
Missing badge numbers (stored on only ~66 elements) are filled from the inventory.

Output: _imp_brentwood_100/items.json  + staged /img/iN.ext  (large variant).
Run:  python3 migration/build_brentwood_100.py
"""
import json, os, re, shutil

DUMP = "/srv/apps/brentwood/notes/brentwood-100-db-dump.json"
INV  = "/srv/apps/brentwood/notes/brentwood-100-content-inventory.md"
PUB  = "/srv/apps/brentwood/laravel/storage/app/public"
IMP  = "/srv/apps/brentwooddev/wp-content/_imp_brentwood_100"
CONTAINER_IMP = "/var/www/html/wp-content/_imp_brentwood_100"
os.makedirs(IMP + "/img", exist_ok=True)

def slugify(s):
    s = re.sub(r'<[^>]+>', '', s or '').strip().lower()
    s = re.sub(r'[‘’“”]', '', s)
    s = re.sub(r'[^a-z0-9]+', '-', s).strip('-')
    return s or 'item'

# --- inventory: title -> badge number (fills the ~21 numbers not stored in DB) ---
inv_num = {}
if os.path.isfile(INV):
    for m in re.finditer(r'^-\s*\*\*#(\d+)\*\*\s*\[([^\]]+)\]', open(INV).read(), re.M):
        inv_num.setdefault(slugify(m.group(2)), m.group(1))

_seq = {'n': 0}
def resolve_disk(*cands):
    for x in cands:
        if x and x.strip():
            dd = os.path.join(PUB, x.lstrip('/'))
            if os.path.isfile(dd):
                return dd
    return None
_staged = {}
def stage(ph):
    """Stage a photo dict's best-available file; return the container-visible path."""
    disk = resolve_disk(ph.get('large'), ph.get('retina'), ph.get('medium'), ph.get('small'))
    if not disk:
        return None
    if disk in _staged:
        dest = _staged[disk]
    else:
        _seq['n'] += 1
        ext = os.path.splitext(disk)[1] or '.jpg'
        dest = f"i{_seq['n']}{ext}"
        shutil.copyfile(disk, os.path.join(IMP, 'img', dest))
        _staged[disk] = dest
    return {
        'file': CONTAINER_IMP + '/img/' + dest,
        'alt': (ph.get('alt') or '').strip(),
        'caption': (ph.get('subtitle') or '').strip(),
    }

def stage_all(photos):
    out = []
    for ph in photos or []:
        s = stage(ph)
        if s:
            out.append(s)
    return out

d = json.load(open(DUMP))
els = d['elements']

def f(e, k):
    return (e.get('flags') or {}).get(k)

def body_of(e):
    b = (e.get('body') or '').strip()
    return '' if b in ('', '<p></p>', '<p></p>\n') else b

def number_of(*es):
    for e in es:
        if e and e.get('number'):
            return str(e['number'])
    for e in es:
        if e and e.get('header'):
            n = inv_num.get(slugify(e['header']))
            if n:
                return n
    return ''

items = []
seen_anchor = {}
def anchor_for(title, number):
    a = slugify(title)
    if a in seen_anchor:
        a = f"{a}-{number}" if number else f"{a}-{seen_anchor[a]+1}"
    seen_anchor[a] = seen_anchor.get(a, 0) + 1
    return a

n_modal = n_inline = n_montage = n_draft = n_video = 0
for i, e in enumerate(els):
    is_tile = f(e, 'expandable') is None and f(e, 'randomize') == 1
    unlisted = bool(f(e, 'unlisted'))
    photos = e.get('photos') or []
    has_link = bool(photos and photos[0].get('link'))

    # Skip the intro slideshow (sort_order 1, not a tile) and non-tile story bodies.
    if not is_tile and not unlisted:
        continue

    status = 'draft' if unlisted else 'publish'

    if has_link:
        # MODAL: pair with the following element if it is the modal story.
        story = els[i + 1] if i + 1 < len(els) and f(els[i + 1], 'expandable') == 'modal' else None
        title = (story.get('header') if story else '') or (e.get('header') or '')
        number = number_of(story, e)
        cover = stage(photos[0]) if photos else None
        gallery = stage_all(story.get('photos') if story else [])
        items.append({
            'src_key': f"{(story or e)['type']}:{(story or e)['content_id']}",
            'type': 'modal', 'status': status,
            'title': title.strip(), 'number': number,
            'body': body_of(story) if story else '',
            'cover': cover, 'caption': (photos[0].get('subtitle') or '').strip() if photos else '',
            'gallery': gallery, 'span': int(photos[0].get('span') or 1) if photos else 1,
            'anchor': anchor_for(title, number),
        })
        n_draft += status == 'draft'; n_modal += status == 'publish'
    else:
        b = body_of(e)
        title = (e.get('header') or '').strip()
        number = number_of(e)
        cover = stage(photos[0]) if photos else None
        gallery = stage_all(photos[1:])
        typ = 'inline' if b else 'montage'
        if e['type'] == 'EmbedVideo':
            typ = 'inline'; n_video += 1
        resolved_title = (title or (cover['caption'] if cover else '')).strip()
        items.append({
            'src_key': f"{e['type']}:{e['content_id']}",
            'type': typ, 'status': status,
            'title': resolved_title, 'number': number,
            'body': b,
            'cover': cover, 'caption': (photos[0].get('subtitle') or '').strip() if photos else '',
            'gallery': gallery, 'span': int(photos[0].get('span') or 1) if photos else 1,
            'needs_video': e['type'] == 'EmbedVideo',
            'anchor': anchor_for(resolved_title or 'item', number),
        })
        if status == 'draft':
            n_draft += 1
        elif typ == 'inline':
            n_inline += 1
        else:
            n_montage += 1

json.dump(items, open(IMP + '/items.json', 'w'), indent=1)
os.system("chmod -R 775 %s 2>/dev/null; find %s -type f -exec chmod 664 {} + 2>/dev/null" % (IMP, IMP))
missing_num = sum(1 for it in items if it['status'] == 'publish' and not it['number'])
print(f"items={len(items)}  modal={n_modal} inline={n_inline} montage={n_montage} draft={n_draft} "
      f"(embed-video items={n_video})")
print(f"images_staged={_seq['n']}  published_missing_number={missing_num}")
