#!/usr/bin/env python3
import csv, json, os, zipfile
from datetime import datetime, timezone
from pathlib import Path

OUT = Path('/Users/iggy/.hermes/profiles/ignite_team/outbound')
PLAN = OUT / 'krb-empty-image-remediation-plan-v1.json'
CTX = OUT / 'krb-empty-image-component-props-current-v1-prop-context.json'
PAGES = OUT / 'krb_page_client_audit' / 'all_pages.json'
PAGE_ASSETS = OUT / 'krb-image-grab' / 'krb-webflow-page-images-assets-readback.json'
OTHER_ASSETS = OUT / 'krb-image-grab' / 'krb-webflow-other-static-images-assets-readback.json'
SITE_ID = '6a1e37436b332da28ecc3001'
PAGE_IMAGES_FOLDER_ID = '6a34e1160daf7376173c7071'

plan = json.load(open(PLAN))
ctx = json.load(open(CTX))
pages = json.load(open(PAGES))
assets = {a['id']: a for fp in [PAGE_ASSETS, OTHER_ASSETS] for a in json.load(open(fp))}
page_by_id = {p['id']: p for p in pages}

records = [dict(r) for r in plan['records']]

# Promote three Admissions Gateway CTA manual-review rows to ready-existing-asset by following their link page IDs.
# These are content-bearing cards and the linked target pages already have page-image hero assets in the Page Images folder.
asset_by_path = {
    '/boarding/international': {
        'assetId': '6a34e3cc825c8fb9f606dd19',
        'assetName': 'boarding__international__KRB-Website-090621_289.jpg',
        'sourceFile': 'KRB-Website-090621_289.jpg',
        'sourceUrl': 'https://www.krb.nsw.edu.au/app/uploads/2021/09/KRB-Website-090621_289.jpg',
    },
    '/admissions/enrolment': {
        'assetId': '6a34e3a66a41d050704c90bc',
        'assetName': 'admissions__enrolment__kincoppal2_130.jpg',
        'sourceFile': 'kincoppal2_130.jpg',
        'sourceUrl': 'https://www.krb.nsw.edu.au/app/uploads/2023/01/kincoppal2_130.jpg',
    },
    '/admissions/fees': {
        'assetId': '6a34e3aac5f6e7958815795f',
        'assetName': 'admissions__fees__G2A9388-1.jpg',
        'sourceFile': 'G2A9388-1.jpg',
        'sourceUrl': 'https://www.krb.nsw.edu.au/app/uploads/2021/04/G2A9388-1.jpg',
    },
}

for page in ctx['pages']:
    if page.get('path') != '/admissions':
        continue
    for comp in page.get('components', []):
        if comp.get('componentName') != 'Section / Gateway CTA':
            continue
        props = comp.get('props', [])
        heading = ''
        link_page_id = ''
        for p in props:
            group = (p.get('display') or {}).get('group') or p.get('group') or ''
            label = (p.get('display') or {}).get('label') or p.get('label') or ''
            val = p.get('value') or {}
            if group == 'Text' and label == 'Heading' and isinstance(val, dict):
                heading = ((val.get('value') or '') if isinstance(val.get('value'), str) else '')
            if group == 'Link' and label == 'Link' and isinstance(val, dict):
                v = val.get('value') or {}
                to = v.get('to') or {}
                link_page_id = to.get('pageId') or ''
        linked = page_by_id.get(link_page_id)
        if not linked:
            continue
        linked_path = linked.get('publishedPath') or ('/' + linked.get('slug',''))
        if linked_path not in asset_by_path:
            continue
        # find the existing record for this component/prop
        for r in records:
            if r['pageId'] == page['pageId'] and r['componentId'] == comp['idString'] and r['propId'] == 'f81da0f5-95ff-a2fa-d2f3-abfccbad31a1':
                info = asset_by_path[linked_path]
                asset = assets[info['assetId']]
                r.update({
                    'status': 'ready-existing-asset',
                    'reason': f'Gateway CTA card links to {linked.get("title")} ({linked_path}); using linked page hero/page-image asset.',
                    'sourceUrl': info['sourceUrl'],
                    'sourceFile': info['sourceFile'],
                    'assetId': info['assetId'],
                    'assetName': info['assetName'],
                    'assetFolder': asset.get('folderId') or PAGE_IMAGES_FOLDER_ID,
                    'assetUrl': asset.get('hostedUrl') or '',
                    'matchType': 'linked-page-hero-asset',
                    'confidence': 'high',
                    'linkedPageId': link_page_id,
                    'linkedPath': linked_path,
                    'linkedTitle': linked.get('title'),
                    'context': heading or r.get('context') or '',
                })

# enrich asset URLs/names/folders for all existing ready rows
for r in records:
    aid = r.get('assetId')
    if aid and aid in assets:
        a = assets[aid]
        r.setdefault('assetUrl', a.get('hostedUrl') or '')
        if not r.get('assetUrl'):
            r['assetUrl'] = a.get('hostedUrl') or ''
        if not r.get('assetName'):
            r['assetName'] = a.get('displayName') or ''
        if not r.get('assetFolder'):
            r['assetFolder'] = a.get('folderId') or ''

ready = [r for r in records if r['status'] == 'ready-existing-asset']
needs_upload = [r for r in records if r['status'] == 'needs-upload']
skipped = [r for r in records if r['status'] == 'skip-empty-card']
manual = [r for r in records if r['status'] == 'manual-review']

# targets for the Code Lab script: ready/existing assets only
def element_id(component_id):
    return component_id.split(':', 1)[1] if ':' in component_id else component_id

targets = []
for r in ready:
    targets.append({
        'title': r['pageTitle'],
        'path': r['path'],
        'pageId': r['pageId'],
        'componentInstanceId': r['componentId'],
        'elementId': element_id(r['componentId']),
        'componentName': r['componentName'],
        'componentContext': r.get('context') or '',
        'propId': r['propId'],
        'propLabel': r['propLabel'],
        'assetId': r['assetId'],
        'assetDisplayName': r.get('assetName') or assets.get(r['assetId'], {}).get('displayName') or r.get('sourceFile') or '',
        'assetUrl': r.get('assetUrl') or assets.get(r['assetId'], {}).get('hostedUrl') or '',
        'assetFolder': r.get('assetFolder') or assets.get(r['assetId'], {}).get('folderId') or '',
        'sourceFile': r.get('sourceFile') or '',
        'sourceUrl': r.get('sourceUrl') or '',
        'matchType': r.get('matchType') or '',
        'confidence': r.get('confidence') or '',
        'reason': r.get('reason') or '',
    })

generated_at = datetime.now(timezone.utc).isoformat().replace('+00:00','Z')
summary = {
    'generatedAt': generated_at,
    'siteId': SITE_ID,
    'inputPlan': str(PLAN),
    'targetsReadyExistingAsset': len(targets),
    'needsUploadBeforeWrite': len(needs_upload),
    'skippedPlaceholderEmptyCards': len(skipped),
    'manualReviewRemaining': len(manual),
    'notes': [
        'Script target list contains only existing Webflow asset IDs; no upload-needed or manual-review rows are written.',
        'Three Admissions Gateway CTA cards were promoted from manual-review after resolving their Link/Link page IDs to existing linked-page hero assets.',
        'Hero upload-needed rows use the Page Images folder and prefixed display names but have not been uploaded by this package.',
    ],
}

json_out = OUT / 'krb-empty-image-remediation-reviewed-v2.json'
json_out.write_text(json.dumps({'summary': summary, 'targets': targets, 'needsUpload': needs_upload, 'skipped': skipped, 'manualReview': manual}, indent=2), encoding='utf-8')

csv_out = OUT / 'krb-empty-image-remediation-reviewed-v2.csv'
with open(csv_out, 'w', newline='', encoding='utf-8') as f:
    fields = ['status','title','path','componentName','componentInstanceId','propLabel','propId','assetId','assetDisplayName','assetFolder','sourceFile','sourceUrl','matchType','confidence','reason']
    w = csv.DictWriter(f, fieldnames=fields)
    w.writeheader()
    for status, rows in [('ready-existing-asset', targets), ('needs-upload', needs_upload), ('manual-review', manual), ('skip-empty-card', skipped)]:
        for r in rows:
            w.writerow({
                'status': status,
                'title': r.get('title') or r.get('pageTitle'),
                'path': r.get('path'),
                'componentName': r.get('componentName'),
                'componentInstanceId': r.get('componentInstanceId') or r.get('componentId'),
                'propLabel': r.get('propLabel'),
                'propId': r.get('propId'),
                'assetId': r.get('assetId'),
                'assetDisplayName': r.get('assetDisplayName') or r.get('assetName'),
                'assetFolder': r.get('assetFolder') or r.get('proposedUploadFolderId'),
                'sourceFile': r.get('sourceFile'),
                'sourceUrl': r.get('sourceUrl'),
                'matchType': r.get('matchType'),
                'confidence': r.get('confidence'),
                'reason': r.get('reason'),
            })

upload_csv = OUT / 'krb-empty-image-remediation-needs-upload-v2.csv'
with open(upload_csv, 'w', newline='', encoding='utf-8') as f:
    fields = ['title','path','componentName','propLabel','sourceFile','sourceUrl','proposedUploadFolderId','proposedUploadDisplayName','reason']
    w = csv.DictWriter(f, fieldnames=fields)
    w.writeheader()
    for r in needs_upload:
        w.writerow({k: r.get(k,'') for k in fields})

md_out = OUT / 'krb-empty-image-remediation-reviewed-v2.md'
lines = []
lines += ['# KRB empty image prop remediation reviewed v2', '', f'Generated: {generated_at}', '']
lines += ['## Summary', '', f'- Ready existing-asset Code Lab writes: **{len(targets)}**', f'- Needs upload before write: **{len(needs_upload)}**', f'- Skipped placeholder/empty repeated-card props: **{len(skipped)}**', f'- Manual review remaining: **{len(manual)}**', '']
lines += ['## Script scope', '', '- Writes only ready/existing Webflow assets listed in `krb-empty-image-prop-repair-existing-assets-v2.js`.', '- Default is `dryRun: true` and `runMode: current-page`.', '- It refuses to overwrite a non-empty image prop when `requireEmptyImageBeforeWrite` is true.', '- It does not publish, delete, create components, edit styles, or upload assets.', '']
lines += ['## Ready existing-asset writes', '']
for t in targets:
    lines.append(f"- {t['title']} `{t['path']}` — {t['componentName']} / {t['propLabel']} → `{t['assetDisplayName']}` (`{t['assetId']}`), confidence {t['confidence']}, match {t['matchType']}")
lines += ['', '## Needs upload before write', '']
for r in needs_upload:
    lines.append(f"- {r['pageTitle']} `{r['path']}` — needs `{r['sourceFile']}` from {r['sourceUrl']} → Page Images folder `{r['proposedUploadFolderId']}`, proposed name `{r['proposedUploadDisplayName']}`")
lines += ['', '## Manual review remaining', '']
for r in manual:
    lines.append(f"- {r['pageTitle']} `{r['path']}` — {r['componentName']} / {r['propLabel']}: {r['reason']}")
lines += ['', '## Skipped placeholders', '']
for r in skipped:
    lines.append(f"- {r['pageTitle']} `{r['path']}` — {r['componentName']} / {r['propLabel']}: {r['reason']}")
md_out.write_text('\n'.join(lines) + '\n', encoding='utf-8')

# Generate Code Lab JS.
js_out = OUT / 'krb-empty-image-prop-repair-existing-assets-v2.js'
js_targets = json.dumps(targets, indent=2)
js = f"""// KRB empty image component prop repair — existing assets v2\n// Generated: {generated_at}\n// Site: Kincoppal-Rose Bay Webflow site {SITE_ID}\n// Scope: {len(targets)} empty image-shaped component props with confirmed existing Webflow assets.\n// Excludes: {len(needs_upload)} upload-needed rows, {len(manual)} manual-review rows, {len(skipped)} placeholder/empty-card rows.\n// Intended execution: Webflow Designer API Playground / Code Lab only. No publish, no delete, no component/style/page creation.\n// Default is dry-run/current-page. Set CONFIG.dryRun=false only after reviewing dry-run output.\n(async () => {{\n  const EXPECTED_SITE_ID = '{SITE_ID}';\n  const TARGETS = {js_targets};\n\n  const CONFIG = {{\n    dryRun: true,\n    runMode: 'current-page', // 'current-page' or 'all-pages'\n    switchDelayMs: 700,\n    resultPrefix: 'KRB_EMPTY_IMAGE_PROP_REPAIR_V2_RESULT',\n    currentPageTargetPath: '', // optional fallback if getCurrentPage() is unavailable; e.g. '/admissions'\n    currentPageTargetTitle: '',\n    requireEmptyImageBeforeWrite: true,\n    stopOnFirstError: false,\n  }};\n\n  const report = {{\n    scope: 'KRB empty image component prop repair — existing assets v2',\n    expectedSiteId: EXPECTED_SITE_ID,\n    dryRun: CONFIG.dryRun,\n    runMode: CONFIG.runMode,\n    targetCount: TARGETS.length,\n    inspected: [],\n    proposed: [],\n    applied: [],\n    skipped: [],\n    warnings: [],\n    errors: [],\n  }};\n\n  const asArray = (v) => Array.isArray(v) ? v : [];\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const norm = (v) => String(v || '').trim().toLowerCase().replace(/&amp;/g, '&').replace(/[^a-z0-9]+/g, ' ').trim();\n  const compact = (v) => norm(v).replace(/\\s+/g, '');\n  const safe = async (label, fn, fallback = null) => {{\n    try {{ return await fn(); }}\n    catch (err) {{ report.warnings.push(`${{label}}: ${{err && err.message || err}}`); return fallback; }}\n  }};\n  async function describe(x) {{\n    if (typeof helpers !== 'undefined' && helpers && typeof helpers.describe === 'function') return await safe('helpers.describe', () => helpers.describe(x), {{}});\n    return {{}};\n  }}\n  async function getCurrentSiteId() {{\n    for (const fn of ['getSiteId', 'getCurrentSiteId']) if (typeof webflow[fn] === 'function') {{\n      const val = await safe(`webflow.${{fn}}`, () => webflow[fn](), null);\n      if (val) return String(val);\n    }}\n    const site = typeof webflow.getSite === 'function' ? await safe('webflow.getSite', () => webflow.getSite(), null) : null;\n    if (site) {{ const d = await describe(site); return String(d.id || d._id || site.id || site._id || ''); }}\n    return null;\n  }}\n  async function flattenPages(items, out = []) {{\n    for (const item of asArray(items)) {{\n      const d = await describe(item);\n      const type = String(d.type || item.type || item.constructor && item.constructor.name || '').toLowerCase();\n      const maybePage = (typeof item.getName === 'function') || (typeof item.getTitle === 'function') || d.title || d.name || item.name || item.title || item.id || item._id;\n      if (maybePage && !type.includes('folder')) out.push(item);\n      for (const key of ['children', 'pages', 'items']) if (Array.isArray(item && item[key])) await flattenPages(item[key], out);\n      for (const method of ['getChildren', 'getPages']) if (item && typeof item[method] === 'function') {{\n        const kids = await safe(`pageFolder.${{method}}`, () => item[method](), []);\n        if (kids && kids.length) await flattenPages(kids, out);\n      }}\n    }}\n    return out;\n  }}\n  async function getPages() {{\n    if (typeof webflow.getAllPagesAndFolders === 'function') return await flattenPages(await webflow.getAllPagesAndFolders());\n    if (typeof webflow.getAllPages === 'function') return asArray(await webflow.getAllPages());\n    if (typeof webflow.getPages === 'function') return asArray(await webflow.getPages());\n    return [];\n  }}\n  async function pageInfo(page) {{\n    const d = await describe(page);\n    const id = await safe('page.getId', () => typeof page.getId === 'function' ? page.getId() : (page.id || page._id || d.id || d._id), page.id || page._id || d.id || d._id || null);\n    const name = await safe('page.getName', () => typeof page.getName === 'function' ? page.getName() : (page.name || d.name || d.title), page.name || d.name || d.title || '');\n    const title = await safe('page.getTitle', () => typeof page.getTitle === 'function' ? page.getTitle() : (d.title || name), d.title || name || '');\n    const slug = await safe('page.getSlug', () => typeof page.getSlug === 'function' ? page.getSlug() : (page.slug || d.slug || ''), page.slug || d.slug || '');\n    const publishPath = await safe('page.getPublishPath', () => typeof page.getPublishPath === 'function' ? page.getPublishPath() : (d.publishPath || d.path || ''), d.publishPath || d.path || '');\n    return {{ id: String(id || ''), name: String(name || ''), title: String(title || name || ''), slug: String(slug || ''), publishPath: String(publishPath || '') }};\n  }}\n  async function switchToPage(page) {{\n    if (typeof webflow.switchPage === 'function') {{ await webflow.switchPage(page); await sleep(CONFIG.switchDelayMs); return; }}\n    if (page && typeof page.switchTo === 'function') {{ await page.switchTo(); await sleep(CONFIG.switchDelayMs); return; }}\n    throw new Error('No page switching API found.');\n  }}\n  async function getChildren(el) {{ return el && typeof el.getChildren === 'function' ? asArray(await safe('getChildren', () => el.getChildren(), [])) : []; }}\n  async function getSlots(el) {{ return el && typeof el.getSlots === 'function' ? asArray(await safe('getSlots', () => el.getSlots(), [])) : []; }}\n  async function getComponentName(el) {{\n    const c = el && typeof el.getComponent === 'function' ? await safe('getComponent', () => el.getComponent(), null) : null;\n    if (!c) return null;\n    const d = await describe(c);\n    return await safe('component.getName', () => typeof c.getName === 'function' ? c.getName() : (d.name || d.displayName || c.name), d.name || d.displayName || c.name || null);\n  }}\n  async function collect(el, depth = 0, out = [], seen = new Set()) {{\n    if (!el) return out;\n    const d = await describe(el);\n    const id = await safe('element id', () => typeof el.getId === 'function' ? el.getId() : (d.id || d._id || null), d.id || d._id || null);\n    if (id && seen.has(id)) return out;\n    if (id) seen.add(id);\n    const componentName = await getComponentName(el);\n    out.push({{ el, depth, id: String(id || ''), desc: d, componentName }});\n    for (const child of await getChildren(el)) await collect(child, depth + 1, out, seen);\n    for (const slot of await getSlots(el)) for (const child of await getChildren(slot)) await collect(child, depth + 2, out, seen);\n    return out;\n  }}\n  function propLabel(prop) {{ return String(prop && (prop.display && prop.display.label || prop.label || prop.name || prop.propId || prop.id || '') || ''); }}\n  function propPath(prop) {{\n    const group = prop && (prop.display && prop.display.group || prop.group || '');\n    const label = propLabel(prop);\n    return prop && prop.path || (group ? `${{group}}/${{label}}` : label);\n  }}\n  function propId(prop) {{ return prop && (prop.propId || prop.id || null); }}\n  function isImageProp(prop) {{ return String(prop && (prop.valueType || prop.type || '') || '').toLowerCase().includes('image'); }}\n  async function getProps(el) {{ return el && typeof el.searchProps === 'function' ? asArray(await safe('searchProps', () => el.searchProps(), [])) : []; }}\n  async function getPropCurrentValue(prop) {{\n    for (const fn of ['getValue', 'getResolvedValue']) if (prop && typeof prop[fn] === 'function') {{\n      const v = await safe(`prop.${{fn}}`, () => prop[fn](), undefined);\n      if (v !== undefined) return v;\n    }}\n    return prop && ('value' in prop ? prop.value : (prop.currentValue || prop.defaultValue || null));\n  }}\n  function unwrapStaticValue(value) {{\n    if (value && typeof value === 'object' && value.sourceType === 'static' && Object.prototype.hasOwnProperty.call(value, 'value')) return value.value;\n    return value;\n  }}\n  function isBlankImageValue(value) {{\n    value = unwrapStaticValue(value);\n    if (value == null) return true;\n    if (typeof value === 'string') return value.trim() === '';\n    if (typeof value === 'object') return Object.keys(value).length === 0 || (!value.id && !value.assetId && !value.url && !value.src && !value.hostedUrl);\n    return false;\n  }}\n  async function resolveAsset(target) {{\n    for (const fn of ['getAllAssets', 'getAssets']) if (typeof webflow[fn] === 'function') {{\n      const assets = asArray(await safe(`webflow.${{fn}}`, () => webflow[fn](), []));\n      const found = assets.find((a) => String(a.id || a._id || '') === target.assetId)\n        || assets.find((a) => compact(a.displayName || a.name || a.fileName || '').includes(compact(target.assetDisplayName || target.sourceFile)));\n      if (found) return found;\n    }}\n    return null;\n  }}\n  function imageValueAttempts(target, asset) {{\n    const id = target.assetId;\n    const url = target.assetUrl;\n    const attempts = [];\n    if (asset) attempts.push(asset);\n    attempts.push(\n      {{ id, objectType: 'Asset' }},\n      {{ assetId: id, objectType: 'Asset' }},\n      {{ id, url, hostedUrl: url, objectType: 'Asset', displayName: target.assetDisplayName }},\n      {{ assetId: id, url, hostedUrl: url, objectType: 'Asset', displayName: target.assetDisplayName }},\n      id,\n      url\n    );\n    return attempts.filter(Boolean);\n  }}\n  function nodeMatchesTarget(node, target) {{\n    if (!node || !node.el) return false;\n    const id = String(node.id || '');\n    return (id && (id === target.elementId || id === target.componentInstanceId || target.componentInstanceId.endsWith(':' + id)))\n      && (!target.componentName || node.componentName === target.componentName);\n  }}\n  async function processTarget(target, page, pageMeta, alreadyOnPage = false) {{\n    const pageReport = {{ title: target.title, path: target.path, pageId: target.pageId, componentName: target.componentName, elementId: target.elementId, propId: target.propId, assetId: target.assetId, status: 'pending', imageBefore: null, assetFoundInRuntime: false }};\n    report.inspected.push(pageReport);\n    if (!alreadyOnPage) await switchToPage(page);\n    const root = await webflow.getRootElement();\n    const nodes = await collect(root);\n    const node = nodes.find((n) => nodeMatchesTarget(n, target));\n    if (!node) {{\n      pageReport.status = 'skipped-component-not-found';\n      report.skipped.push({{ target, reason: 'Target component instance not found on page.' }});\n      return;\n    }}\n    const ps = await getProps(node.el);\n    const prop = ps.find((p) => propId(p) === target.propId) || ps.find((p) => compact(propPath(p)) === compact(target.propLabel));\n    if (!prop) {{\n      pageReport.status = 'skipped-prop-not-found';\n      report.skipped.push({{ target, reason: 'Target prop not found on component instance.', availableProps: ps.map(propPath) }});\n      return;\n    }}\n    if (!isImageProp(prop)) {{\n      pageReport.status = 'skipped-not-image-prop';\n      report.skipped.push({{ target, reason: `Target prop is not image-shaped: ${{prop.valueType || prop.type || 'unknown'}}` }});\n      return;\n    }}\n    const before = await getPropCurrentValue(prop);\n    pageReport.imageBefore = before;\n    if (CONFIG.requireEmptyImageBeforeWrite && !isBlankImageValue(before)) {{\n      pageReport.status = 'skipped-existing-image';\n      report.skipped.push({{ target, reason: 'Image prop is no longer empty; left unchanged.', imageBefore: before }});\n      return;\n    }}\n    const asset = await resolveAsset(target);\n    pageReport.assetFoundInRuntime = Boolean(asset);\n    const change = {{ title: target.title, path: target.path, pageId: target.pageId, componentName: target.componentName, elementId: target.elementId, propId: target.propId, propPath: propPath(prop), assetId: target.assetId, assetDisplayName: target.assetDisplayName, dryRun: CONFIG.dryRun }};\n    report.proposed.push(change);\n    if (CONFIG.dryRun) {{ pageReport.status = 'dry-run-proposed'; return; }}\n    let success = false, lastError = null, valueShape = null;\n    for (const value of imageValueAttempts(target, asset)) {{\n      try {{\n        await node.el.setProps([{{ propId: propId(prop), value }}]);\n        success = true;\n        valueShape = typeof value === 'string' ? 'string' : Object.keys(value || {{}});\n        break;\n      }} catch (err) {{ lastError = err && err.message || String(err); }}\n    }}\n    if (!success) throw new Error(`All image value shapes rejected for ${{target.title}} / ${{target.componentName}}: ${{lastError}}`);\n    await sleep(150);\n    pageReport.status = 'applied';\n    report.applied.push({{ ...change, valueShape }});\n  }}\n\n  try {{\n    const siteId = await getCurrentSiteId();\n    if (siteId && siteId !== EXPECTED_SITE_ID) throw new Error(`Refusing to run on site ${{siteId}}; expected ${{EXPECTED_SITE_ID}}.`);\n    if (CONFIG.runMode === 'current-page') {{\n      const currentPage = typeof webflow.getCurrentPage === 'function' ? await safe('webflow.getCurrentPage', () => webflow.getCurrentPage(), null) : null;\n      const currentInfo = currentPage ? await pageInfo(currentPage) : null;\n      let selectedTargets = TARGETS.filter((t) => currentInfo && (String(currentInfo.id) === t.pageId || norm(currentInfo.publishPath) === norm(t.path) || norm(currentInfo.slug) === norm(t.path.replace(/^\\//, '')) || norm(currentInfo.title) === norm(t.title)));\n      if (!selectedTargets.length && (CONFIG.currentPageTargetPath || CONFIG.currentPageTargetTitle)) {{\n        selectedTargets = TARGETS.filter((t) => (CONFIG.currentPageTargetPath && norm(t.path) === norm(CONFIG.currentPageTargetPath)) || (CONFIG.currentPageTargetTitle && norm(t.title) === norm(CONFIG.currentPageTargetTitle)));\n      }}\n      if (!selectedTargets.length) throw new Error(`Current-page mode found no targets. Current page: ${{JSON.stringify(currentInfo)}}. If getCurrentPage() is unavailable, set CONFIG.currentPageTargetPath while on a target page.`);\n      for (const target of selectedTargets) {{\n        try {{ await processTarget(target, currentPage, currentInfo || {{ note: 'current page; matched by CONFIG fallback' }}, true); }}\n        catch (err) {{ report.errors.push({{ target, error: err && err.stack || String(err) }}); if (CONFIG.stopOnFirstError) break; }}\n      }}\n    }} else if (CONFIG.runMode === 'all-pages') {{\n      const pages = await getPages();\n      if (!pages.length) throw new Error('All-pages mode needs webflow.getAllPagesAndFolders(), getAllPages(), or getPages(); none returned pages in this Code Lab environment. Use current-page mode page-by-page instead.');\n      const pageRows = [];\n      for (const page of pages) pageRows.push({{ page, info: await pageInfo(page) }});\n      for (const target of TARGETS) {{\n        const match = pageRows.find((r) => r.info.id === target.pageId)\n          || pageRows.find((r) => norm(r.info.publishPath) === norm(target.path))\n          || pageRows.find((r) => norm(r.info.slug) === norm(target.path.replace(/^\\//, '')))\n          || pageRows.find((r) => norm(r.info.title) === norm(target.title));\n        if (!match) {{ report.skipped.push({{ target, reason: 'Target page not found in Webflow pages list.' }}); continue; }}\n        try {{ await processTarget(target, match.page, match.info); }}\n        catch (err) {{ report.errors.push({{ target, error: err && err.stack || String(err) }}); if (CONFIG.stopOnFirstError) break; }}\n      }}\n    }} else {{\n      throw new Error(`Unknown CONFIG.runMode: ${{CONFIG.runMode}}`);\n    }}\n  }} catch (err) {{\n    report.errors.push({{ fatal: err && err.stack || String(err) }});\n  }} finally {{\n    console.log(CONFIG.resultPrefix, JSON.stringify(report, null, 2));\n    return report;\n  }}\n}})();\n"""
js_out.write_text(js, encoding='utf-8')

zip_out = OUT / 'krb-empty-image-remediation-reviewed-v2.zip'
with zipfile.ZipFile(zip_out, 'w', compression=zipfile.ZIP_DEFLATED) as z:
    for fp in [json_out, csv_out, upload_csv, md_out, js_out]:
        z.write(fp, fp.name)

print(json.dumps({
    'summary': summary,
    'files': [str(p) for p in [json_out, csv_out, upload_csv, md_out, js_out, zip_out]],
}, indent=2))
