import copy
import hashlib
import json
import math
import zipfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / 'data/model-evidence-2026-09-18'
WEB = ROOT / 'data/website-model-evidence-2026-09-18'
DATE = '2026-09-18'
SOURCE = 'archive-model-checks'


def load(path):
    return json.loads(path.read_text())


def write(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + '\n')


def artifact(path, download=None):
    content = path.read_bytes()
    record = dict(path=str(path.relative_to(ROOT)), sha256=hashlib.sha256(content).hexdigest(), bytes=len(content))
    if download:
        record['download'] = download
        target = ROOT / 'website/public' / download.lstrip('/')
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_bytes(content)
    return record


def main():
    prior = load(ROOT / 'data/website-site-review-2026-09-18/catalog.json')
    catalog = copy.deepcopy(prior)
    cases = load(DATA / 'case-index.json')
    demand = load(DATA / 'demand-checks.json')
    validation = load(DATA / 'validation.json')
    assert validation['passed'] and len(cases) == 32
    registry = {row['source_id']: row for row in load(ROOT / 'data/nuclear-delivery-2026-09-17/source-catalog.json')['sources']}
    selections = [
        ('bfe-energy-balance-2025', 'bfe-7519', 'T25!B70:H71; national electricity balance and final consumption', 'Official national totals; the narrower industry/services survey is a different population.'),
        ('dtu-heat-storage-2024', 'dtu-htes2024-backend', 'PDF p6 capacity; PDF p15 Table 4, 2024 annual heat balance and short-term operating context', 'Measured heat flows at one Danish network store. No Swiss electricity saving is inferred.'),
        ('dea-heat-storage-costs-2025', 'dea-thermal-2025', 'pp17,21,35–36; full-project investment table, subsidy, estimated OPEX and modelled income', 'Implementation-year project costs, before and after grants. Annual income is modelled and operating cost estimated; neither is an audited owner outturn or Swiss offer.'),
        ('drake-landing-lifecycle-2024', 'drake-background', 'Slides 5,16,18,22,25; maintenance, income/cost mismatch and 2024 closure', 'Municipal account of one Canadian community. A failed project does not establish universal failure of thermal storage.'),
        ('hydroleap-operating-limits-2025', 'aramis-doc-73996', 'pp29–31 full-scale hydraulic/temperature tests; pp36–40 fatigue models; pp54–56 refurbishment; pp79–81 hybrid tests', 'Separate full-scale tests, reduced-scale experiments and wear models. No national extra-energy allowance or audited CHF per start is established.'),
    ]
    for sid, original, locator, note in selections:
        record = registry[original]
        assert hashlib.sha256((ROOT / record['path']).read_bytes()).hexdigest() == record['sha256']
        paths = [record['path']] + ([record['text_path']] if record.get('text_path') else [])
        catalog['sources'].append(dict(id=sid, title=record['title'], publisher=record['publisher'], kind='primary', url=record['url'], publicationDate=record.get('publication_date'), edition=record.get('publication_date') or 'Archived edition', retrievedAt=record['retrieved_at_utc'], reviewedAt=DATE, locator=locator, note=note, artifacts=[artifact(ROOT / path) for path in paths]))
    hydro_record = next(row for row in load(ROOT / 'data/followup/source-catalog.json') if row['id'] == 'bieudron-revision-2026')
    assert hashlib.sha256((ROOT / hydro_record['path']).read_bytes()).hexdigest() == hydro_record['sha256']
    catalog['sources'].append(dict(id='bieudron-overhaul-2026', title=hydro_record['title'], publisher=hydro_record['publisher'], kind='primary', url=hydro_record['url'], publicationDate='2026-07-15', edition='Operator works update, 15 July 2026', retrievedAt=hydro_record['retrieved_at'], reviewedAt=DATE, locator='Page 1: scope, approximate duration per unit and total investment; page 2: staged 2024–2026 programme', note='Reported investment for an ongoing overhaul, not a final audited outturn. One unit is revised at a time while the others operate. The publication forecasts November 2026 completion; no completion is inferred here.', artifacts=[artifact(ROOT / hydro_record[key]) for key in ['path', 'text_path']]))
    claims = []

    def claim(id, label, value, unit, status, period, definition, formula, limitation, sources=None):
        row = dict(id=id, label=label, value=value, unit=unit, status=status, period=period, definition=definition, formula=formula, limitation=limitation, sources=sources or [SOURCE], reviewedAt=DATE)
        claims.append(row)
        return id

    limits = 'Deterministic 2035 fixed-charging scenarios with 26 GWp PV, finite pooled hydro, Gösgen/Leibstadt, scheduled maintenance and 98% other nuclear availability. Up to 2 GW imports unless specified. Perfect foresight, smoothed monthly hydro, simplified grid, optimized cyclic starting stocks. No reserve margin, probabilities, owner contracts or full national adequacy proof. The long outage removes Leibstadt for 90 winter days.'
    baseline = [row for row in cases if row['case']['imports'] == 2 and row['case']['extra_twh'] == 0]
    stress = [row for row in baseline if row['case']['forced']]
    routine = [row for row in baseline if not row['case']['forced']]
    assert len(stress) == len(routine) == 13
    claim('weather-screen-energy', 'Additional winter delivery across thirteen weather stress cases', math.ceil(max(r['winter_shortfall_gwh'] for r in stress) / 100) / 10, 'TWh', 'scenario', '2035 demand; 2011/12–2023/24 weather', 'Largest winter gap rounded upward to 0.1 TWh.', 'ceil(max(winter_shortfall_gwh) / 100) / 10 across thirteen matched long-outage cases.', limits)
    claim('weather-screen-power', 'Additional available power across thirteen weather stress cases', math.ceil(max(r['unserved_peak_gw'] for r in stress) * 10) / 10, 'GW', 'scenario', '2035 demand; 2011/12–2023/24 weather', 'Largest hourly annual power gap rounded upward to 0.1 GW.', 'ceil(max(unserved_peak_gw) × 10) / 10 across thirteen matched long-outage cases.', limits)
    growth_cases = [row for row in cases if row['case']['extra_twh'] > 0]
    screen_cases = stress + growth_cases
    for suffix, value, unit, definition, formula in [
        ('energy', math.ceil(max(r['winter_shortfall_gwh'] for r in screen_cases) / 100) / 10, 'TWh', 'Largest winter requirement across thirteen base weather cases plus three data-centre sensitivities, rounded upward.', 'ceil(max(screen winter_shortfall_gwh) / 100) / 10'),
        ('power', math.ceil(max(r['unserved_peak_gw'] for r in screen_cases) * 10) / 10, 'GW', 'Largest hourly requirement across the same cases, rounded upward.', 'ceil(max(screen unserved_peak_gw) × 10) / 10'),
    ]:
        claim('growth-screen-' + suffix, 'Starting supply request including the separate data-centre allowance: ' + suffix, value, unit, 'scenario', '2035 package and three growth sensitivities', definition, formula, limits + 'The growth cases assume the study central 2030 data-centre total stays at 3.2 TWh through 2035, mapped to retained service demand. Only 2012, 2017 and 2022 receive the added-load test; this is a preliminary offer brief, not a full growth/weather adequacy assessment.')
    dc2035 = next(row for row in demand['data_centre_rows'] if row['year'] == 2035 and row['mapping'] == 'services' and row['outcome'] == 'best_guess')
    for suffix, field in [('extra', 'additional_twh'), ('total', 'final_demand_twh')]:
        claim('dc-demand-2035-' + suffix, '2035 annual end-use demand with separate data-centre allowance: ' + suffix, dc2035[field], 'TWh/year', 'scenario', '2035 with 2030 central study DC total held constant', 'Reference-weather annual consumption; either the additional allowance or total including it, as labelled.', demand['dc_formula'], demand['limitation'], [SOURCE, 'bfe-data-centres-2026'])
    claim('weather-routine-max-winter', 'Largest winter gap with routine nuclear assumptions', max(r['winter_shortfall_gwh'] for r in routine) / 1000, 'TWh', 'scenario', '2035 demand; thirteen historical weather years', 'Maximum winter requirement across the same years without the exceptional long outage.', 'max(routine winter_shortfall_gwh) / 1000.', limits + 'Routine still includes scheduled maintenance and the 98% availability allowance.')
    claim('weather-max-summer', 'Largest summer gap in the expanded fixed-charging cases', max(r['summer_shortfall_gwh'] for r in stress), 'GWh', 'scenario', '2035 demand; April–September portions of thirteen weather years', 'Maximum summer shortfall in the long-outage base cases; outside the winter supply quantity.', 'max(summer_shortfall_gwh).', limits)
    for row in baseline:
        case = row['case']
        claim('weather-' + case['id'], f'Winter gap: {case["year"] - 1}/{case["year"]} weather, ' + ('long outage' if case['forced'] else 'routine operation'), row['winter_shortfall_gwh'] / 1000, 'TWh', 'scenario', '2035 demand and historical hydrological-year weather', 'Remaining winter electricity after the specified generation, hydro and imports.', 'sum(hourly unserved electricity in October–March) / 1000.', limits)
    for year in [2012, 2017, 2022]:
        dc = next(r for r in cases if r['case']['id'] == f'{year}-fixed-dc')
        ni = next(r for r in cases if r['case']['id'] == f'{year}-fixed-noimports')
        claim(f'weather-{year}-dc-winter', f'Winter gap with the data-centre allowance, {year} weather', dc['winter_shortfall_gwh'] / 1000, 'TWh', 'scenario', '2035 demand with 2030 study central DC total held constant', 'Long-outage case adds only demand beyond the already-grown embedded service baseline; extra load spread uniformly.', '2035 addition = 3.2 − 2.08 × interpolated services activity × interpolated services intensity; hourly rerun.', limits + 'All-services mapping and a flat 3.2 TWh after 2030 are assumptions, not a 2035 forecast.')
        claim(f'weather-{year}-noimports', f'Annual gap with no imports, {year} weather', ni['unserved_gwh'] / 1000, 'TWh', 'scenario', '2035 demand and historical weather with zero imports', 'Remaining annual electricity gap with the long reactor outage and zero cross-border imports.', 'sum(hourly unserved) / 1000.', limits + 'Import limit is zero in this case. No event probability is assigned.')
    claim('demand-check-2025-model', 'Unchanged demand path at 2025', demand['model_2025_twh'], 'TWh', 'scenario', '2025', 'Existing central path evaluated at 2025, retaining 2024 annual weather assumptions.', 'Run model.demand.calculate at year=2025 with original parameters.', 'One-year comparison; no weather or realized replacement recalibration. The original model was built after 2025.')
    claim('demand-check-2025-observed', 'Recorded national final electricity in 2025', demand['observed_2025_twh'], 'TWh', 'observed', '2025', 'National final electricity, after network losses; pumping already removed from generation.', 'BFE workbook T25!H71 / 1000.', 'Separate from narrower survey or Swissgrid withdrawal boundaries.', ['bfe-energy-balance-2025'])
    claim('demand-check-2025-error', 'Central path above recorded 2025 final consumption', demand['holdout_error_percent'], '%', 'calculated', '2025 comparison', 'Relative difference of unchanged model path and official final electricity.', '(model / observed − 1) × 100.', 'A close annual total does not validate 2050 end-use assumptions.', [SOURCE, 'bfe-energy-balance-2025'])
    claim('demand-population-check', '2050 demand using the EP2060 population input', demand['population_sensitivity_2050_twh'], 'TWh', 'scenario', '2050', 'Original central model with only population changed to 10.3065125 million.', 'calculate(..., population_million=10.3065125).', 'Other sector activity, electrification and efficiency assumptions unchanged.', [SOURCE, 'bfe-ep2060-workbook-2026'])
    claim('demand-population-difference', 'Demand change from the population sensitivity', demand['population_change_twh'], 'TWh', 'calculated', '2050', 'Population sensitivity minus original 93.3656 TWh reference.', 'population_sensitivity_2050_twh − reference_2050_twh.', 'Only the household population dependence represented in this model changes.', [SOURCE, 'bfe-ep2060-workbook-2026'])
    dc_limits = demand['limitation']
    for outcome in ['lower', 'best_guess', 'high_later_possible']:
        row = next(r for r in demand['data_centre_rows'] if r['year'] == 2030 and r['mapping'] == 'services' and r['outcome'] == outcome)
        claim('dc-extra-' + outcome, 'Extra DC demand after embedded service growth: ' + outcome, row['additional_twh'], 'TWh/year', 'scenario', '2030; high case may arrive later', 'Increment above the activity-scaled national model under an all-services retained-electricity mapping.', demand['dc_formula'], dc_limits, [SOURCE, 'bfe-data-centres-2026'])
    embedded = next(r['embedded_dc_twh'] for r in demand['data_centre_rows'] if r['year'] == 2030 and r['mapping'] == 'services')
    claim('dc-embedded-2030', 'Data-centre demand already represented by service growth', embedded, 'TWh/year', 'scenario', '2030', 'Matched 2.08 TWh estimate multiplied by interpolated model activity and retained-electricity intensity.', '2.08 × (1 + .25 × 6/26) × (1 − .1 × 6/26).', dc_limits, [SOURCE, 'bfe-data-centres-2026'])
    high = next(r['total_dc_twh'] for r in demand['data_centre_rows'] if r['outcome'] == 'high_later_possible')
    claim('dc-high-timing', 'High data-centre case, with flexible arrival date', high, 'TWh/year', 'scenario', '2030 or later', 'Study upper sensitivity; printed page 127 explicitly allows arrival after 2030.', 'Published conditional study total, no extrapolation.', 'Author sensitivity, not a promised commissioning or utilization date.', ['bfe-data-centres-2026', SOURCE])
    thermal = next(r for r in load(ROOT / 'data/nuclear-delivery-2026-09-17/thermal-cases.json') if r['id'] == 'hoje-taastrup')
    for suffix, field, scale, unit, status, source in [('capacity', 'storage_capacity_mwh_th', .001, 'GWh heat', 'estimate', 'dtu-heat-storage-2024'), ('charged', 'charge_mwh_th', .001, 'GWh heat', 'observed', 'dtu-heat-storage-2024'), ('delivered', 'discharge_mwh_th', .001, 'GWh heat', 'observed', 'dtu-heat-storage-2024'), ('loss', 'loss_mwh_th', .001, 'GWh heat', 'observed', 'dtu-heat-storage-2024'), ('inventory', 'inventory_change_mwh_th', .001, 'GWh heat', 'observed', 'dtu-heat-storage-2024'), ('cost', 'capex_eur_million', 1, 'million EUR', 'observed', 'dea-heat-storage-costs-2025'), ('grant', 'subsidy_eur_million', 1, 'million EUR', 'observed', 'dea-heat-storage-costs-2025')]:
        claim('heat-store-' + suffix, 'Høje Taastrup: ' + suffix, thermal[field] * scale, unit, status, '2024 heat balance; construction 2020–22 cost reported in 2025', 'Single district-heating store. ' + ('Full-project amount before grants.' if suffix == 'cost' else 'Thermal energy or project-cost boundary as labelled.'), f'Thermal evidence field {field} × {scale}.', 'Heat cannot be treated as deliverable electricity. Danish implementation-year costs, no Swiss quote or national savings credit. Capacity calculated from water volume and temperature window.', [source, SOURCE])
    claim('bieudron-overhaul-months', 'Approximate Bieudron overhaul duration per generating unit', 9, 'months per unit', 'estimate', 'Operator programme reported 15 July 2026', 'Approximate duration of a major overhaul for one generating unit; work is staged across the plant.', 'Operator page 1, duration bullet.', 'Maintenance programme at one plant, not a uniform annual hydro-outage assumption.', ['bieudron-overhaul-2026'])
    claim('bieudron-overhaul-investment', 'Bieudron overhaul programme investment', 54.4, 'million CHF', 'estimate', 'Programme reported 15 July 2026', 'Reported investment for the complete staged overhaul programme, in nominal CHF.', 'Operator page 1, investment bullet.', 'Ongoing programme amount, not an audited final outturn, annual OPEX or replacement-power cost.', ['bieudron-overhaul-2026'])
    dependency_paths = [
        'model/demand.py', 'model/parameters.json', 'data/demand-model-v1/baseline.json',
        'hourly_model/load.py', 'hourly_model/prepare.py', 'hourly_model/dispatch.py', 'hourly_model/config.json',
        'feasibility_model/run.py', 'feasibility_model/dispatch.py', 'portfolio_model/run.py', 'portfolio_model/dispatch.py', 'portfolio_model/parameters.json',
        'data/hourly-model-v1/load-fit.json', 'data/hourly-model-v1/weather-hourly.csv.gz', 'data/hourly-model-v1/weather-audit.json', 'data/hourly-model-v1/hydro-monthly.json',
        'backups/hourly-model-v1-2026-09-10/sources/bfe-monthly-electricity.csv',
        'data/nuclear-delivery-2026-09-17/source-catalog.json', 'data/nuclear-delivery-2026-09-17/demand-statistics.json', 'data/nuclear-delivery-2026-09-17/demand-reconciliation.json', 'data/nuclear-delivery-2026-09-17/thermal-cases.json', 'data/nuclear-delivery-2026-09-17/hydro-evidence.json',
        'data/portfolio-2035-2026-09-18/case-index.json', 'data/website-site-review-2026-09-18/catalog.json',
        'backups/bfe-aramis-2026-09-17/sources/bfe-7519.xlsx',
    ]
    inputs = [artifact(ROOT / path) for path in dependency_paths]
    write(DATA / 'input-manifest.json', dict(reviewedAt=DATE, inputs=inputs, dependencies='model_extensions/requirements.txt'))
    inputs += [artifact(path) for path in sorted((ROOT / 'model_extensions').glob('*.py'))] + [artifact(ROOT / 'model_extensions/requirements.txt')]
    inputs += [artifact(path) for path in sorted((DATA / 'cases').rglob('*')) if path.is_file()]
    inputs += [artifact(DATA / name) for name in ['case-index.json', 'demand-checks.json', 'demand-audit.json', 'validation.json', 'method.md']]
    evidence = dict(reviewedAt=DATE, weather_cases=baseline, sensitivities=[r for r in cases if r not in baseline], demand=demand, thermal=thermal, claims=claims, inputs=inputs, interpretation=[
        'Use the maximum across all thirteen fixed-charging weather tests as a starting supply screen; keep data-centre growth and import loss as separate sensitivities.',
        'The selected historical years are a sample, not probabilities. All use future demand and a hypothetical outage; results are not historical blackouts.',
        'Keep the central 2050 reference while separating prospective load additions. Population sensitivity is small compared with equipment, efficiency and activity assumptions.',
        'Support viable heat-network storage and controls, counting heat service and owner costs. No national electricity saving is credited without a counterfactual and site mapping.',
        'Data-centre high-case timing is qualified by printed p127; study counts and sector/end-use allocation remain estimates.'
    ], limitation=limits)
    write(WEB / 'evidence.json', evidence)
    downloads = [artifact(WEB / 'evidence.json', '/downloads/model-evidence/evidence.json'), artifact(Path(__file__).resolve(), '/downloads/model-evidence/build_website_model_evidence.py'), artifact(DATA / 'validation.json', '/downloads/model-evidence/validation.json'), artifact(DATA / 'method.md', '/downloads/model-evidence/method.md')]
    bundle = WEB / 'reproduction.zip'
    bundled_paths = dependency_paths + [str(p.relative_to(ROOT)) for p in sorted((ROOT / 'model_extensions').glob('*')) if p.is_file()] + [str(p.relative_to(ROOT)) for p in sorted(DATA.rglob('*')) if p.is_file()]
    with zipfile.ZipFile(bundle, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
        for name in sorted(set(bundled_paths)):
            if name.startswith('backups/') and not name.endswith('.csv'):
                continue
            info = zipfile.ZipInfo(name, (2026, 9, 18, 0, 0, 0))
            info.compress_type = zipfile.ZIP_DEFLATED
            archive.writestr(info, (ROOT / name).read_bytes())
    downloads.append(artifact(bundle, '/downloads/model-evidence/reproduction.zip'))
    catalog['sources'].append(dict(id=SOURCE, title='WattInfo archive-to-model checks: demand, thirteen weather years and heat storage', publisher='WattInfo', kind='project-model', url='/downloads/model-evidence/evidence.json', publicationDate=DATE, edition='Additive model evidence review, 18 September 2026', retrievedAt=DATE, reviewedAt=DATE, locator='32 hourly full-year cases; demand checks; measured thermal balance; append-only input manifest and reproduction code', note=limits + ' Source review includes data-centre report printed pp123–127, BFE T25 original cells and cited thermal/hydro sections. Models are independent calculations on archived inputs; no owner profitability or Swiss site permitting is established.', artifacts=downloads + inputs))
    catalog['claims'] += claims
    for key in ['sources', 'claims']:
        assert catalog[key][:len(prior[key])] == prior[key]
    for path in [WEB / 'catalog.json', ROOT / 'website/src/data/catalog.json', ROOT / 'website/public/downloads/catalog.json']:
        write(path, catalog)
    print(len(catalog['sources']), 'sources;', len(catalog['claims']), 'claims')


if __name__ == '__main__':
    main()
