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

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / 'data/portfolio-2035-2026-09-18'
WEB = ROOT / 'data/website-portfolio-2035-2026-09-18'
PUBLIC = ROOT / 'website/public/downloads/plan-2035'
DATE = '2026-09-18'
MODEL = 'portfolio-2035-model'


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):
    raw = path.read_bytes()
    result = dict(path=str(path.relative_to(ROOT)), sha256=hashlib.sha256(raw).hexdigest(), bytes=len(raw))
    if download:
        result['download'] = download
        target = ROOT / 'website/public' / download.lstrip('/')
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_bytes(raw)
    return result


def main():
    catalog = copy.deepcopy(load(ROOT / 'data/website-reliability-2026-09-18/catalog.json'))
    prior = copy.deepcopy(catalog)
    rows = load(DATA / 'case-index.json')
    assert len(rows) == 12
    p = load(ROOT / 'portfolio_model/parameters.json')
    trajectory = load(DATA / 'fleet-trajectory.json')
    titles = {'bfe-charging-2050': 'BFE / EBP: charging infrastructure and vehicle stock to 2050', 'bfs-vehicles-2025': 'BFS: vehicle registrations and stock in 2025', 'noaa-solar-position': 'NOAA: solar-position equations', 'ost-v2x-seminar': 'OST: bidirectional charging services and equipment'}
    titles['wallbox-backup-specification'] = 'Wallbox: home-backup hardware requirement, North American equipment example'
    notes = {'bfe-charging-2050': 'May 2023 scenario, not a fleet observation. The 2035 BEV stock is distinct from all plug-in vehicles. The early new-sales assumptions exceeded the subsequent 2025 result. Selected fleet and charging sections reviewed.', 'bfs-vehicles-2025': 'Official observations: full-calendar-year new registrations versus stock at 30 September 2025. These boundaries are kept explicit in the simplified turnover sensitivity.', 'noaa-solar-position': 'Technical equations reviewed for hourly day/night classification. The chosen Swiss reference point is an analyst assumption; no country-wide identical sunrise is implied.', 'ost-v2x-seminar': 'Selected definitions and architecture reviewed. Publication date unestablished. A dated vehicle compatibility list is not treated as present-day equipment availability.'}
    notes['wallbox-backup-specification'] = 'V1.2 dated 17 April 2024. Footnote 4 requires a separate Power Recovery Unit for backup. Used only to explain the distinction between bidirectional charging and blackout operation; not Swiss compatibility, approval, performance or commercial availability evidence.'
    for source in load(DATA / 'source-catalog.json'):
        files = [artifact(ROOT / source['path']), artifact(ROOT / source['textPath'])]
        assert files[0]['sha256'] == source['sha256']
        catalog['sources'].append(dict(id=source['id'], title=titles[source['id']], publisher=source['publisher'], kind='primary', url=source['url'], publicationDate=source['publicationDate'], edition=source.get('edition') or source['publicationDate'] or 'Undated document retrieved 18 September 2026', retrievedAt=source['retrievedAt'], reviewedAt=DATE, locator=source['locator'], note=notes[source['id']], artifacts=files))
    meta = load(ROOT / 'backups/hourly-model-v1-2026-09-10/sources/bfe-monthly-electricity.csv.metadata.json')
    catalog['sources'].append(dict(id='bfe-monthly-balance-snapshot', title='BFE: monthly Swiss electricity balance, September 2026 snapshot', publisher=meta['publisher'], kind='primary', url=meta['url'], publicationDate=None, edition='Snapshot retrieved 10 September 2026; HTTP last-modified 7 September 2026', retrievedAt=meta['retrieved_at_utc'], reviewedAt=DATE, locator='2024 monthly Erzeugung_Thermische_GWh and Erzeugung_Windkraft_GWh; historical hydro rows reused through archived derived inputs', note='Monthly observed generation. Uniform hourly distribution and continued 2035 thermal/wind output are assumptions. Retaining existing thermal generation does not establish renewable fuel or emissions neutrality.', artifacts=[artifact(ROOT / meta['path']), artifact(ROOT / 'backups/hourly-model-v1-2026-09-10/sources/bfe-monthly-electricity.csv.metadata.json')]))
    claims = []

    def claim(id, label, value, unit, status, definition, formula, limitation, sources=None):
        item = dict(id=id, label=label, value=value, unit=unit, status=status, period='2035 supply assessment, 18 September 2026', definition=definition, formula=formula, limitation=limitation, sources=sources or [MODEL], reviewedAt=DATE)
        claims.append(item)
        catalog['claims'].append(item)

    limits = 'Conditional national-bus screening; perfect foresight, pooled natural hydro, monthly inflow smoothing, no internal network, probabilistic adequacy or owner-economics test. Fleet and connection assumptions are not commitments.'
    claim('plan-demand-2035', 'Reference annual electricity demand in 2035', load(DATA / 'demand-audit.json')['annual_reference_twh'], 'TWh', 'scenario', 'Independent central sector-demand calculation at 2035, end-use electricity before grid losses and extra V2X losses, normalized to calendar 2024.', 'model.demand.calculate(baseline, parameters, 2035, central). Weather-year totals vary.', limits, [MODEL, 'demand-model-v1'])
    claim('plan-pv-2035', 'Proposed solar fleet in 2035', p['solar_gwp'], 'GWp', 'assumption', 'Solar nameplate capacity in the first replacement package.', 'Explicit proposal: 26 GWp.', 'Delivery requires sites, equipment, network access and viable owner returns; peak capacity differs from dependable night power.')
    claim('plan-pv-build-rate', 'Solar addition needed each year', (p['solar_gwp'] - p['solar_2025_gwp']) / 10, 'GWp/year', 'calculated', 'Simple average increase from the estimated end-2025 stock to the proposed end-2035 stock.', '(26 − 9.4986) / 10.', 'Gross stock difference; replacements and changing annual build rates are excluded.', [MODEL, 'bfe-solar-storage-2025'])
    claim('plan-existing-nuclear-2035', 'Remaining nuclear net capacity in the first package', sum(p['nuclear_net_gw'].values()), 'GW', 'scenario', 'Gösgen plus Leibstadt at documented net capacities, conditional on safe, viable continued operation.', '1.010 + 1.233 = 2.243 GW net.', 'Beznau follows announced retirements. No new reactor credited in 2035. Maintenance and outage assumptions reduce modelled generation.', [MODEL, 'bfe-electricity-2025'])
    for id, label, value, unit, definition, source in [('fleet-bev-2025', 'Battery-electric passenger cars in 2025', 249832, 'cars', 'BFS fleet count at 30 September 2025.', 'bfs-vehicles-2025'), ('fleet-new-cars-2025', 'New passenger cars registered during 2025', 232602, 'cars/year', 'BFS full-calendar-year new registrations.', 'bfs-vehicles-2025'), ('fleet-new-bev-share-2025', 'Battery-electric share of new cars in 2025', 22.7, '%', 'New registrations; distinct from the electric share of the whole fleet.', 'bfs-vehicles-2025')]:
        claim(id, label, value, unit, 'observed', definition, 'Published BFS observation.', '2025 observation, not a future replacement rate.', [source])
    claim('fleet-bfe-2035', 'Battery-electric cars in the BFE 2035 scenario', 2100000, 'cars', 'scenario', 'Battery-electric passenger-car stock in the May 2023 charging-infrastructure study.', 'BFE/EBP printed page 17.', 'Conditional adoption scenario; early new-sales expectations exceeded the observed 2025 outcome. Excludes PHEVs.', ['bfe-charging-2050'])
    claim('fleet-sales-paced-2035', 'Electric cars in the sales-paced sensitivity', trajectory[-1]['bev_stock'], 'cars', 'scenario', 'Simple annual stock recurrence using 2025 registration volume and total car stock, with assumed all-BEV new sales by 2035.', 'BEV[t] = BEV[t−1] × (1 − 232602/4839465) + 232602 × BEV new-sales share[t]; share rises linearly from 0.227 to 1 over 2025–2035.', 'Equal retirement rates, fixed total fleet and sales volume, approximate September/calendar-year boundary. Not an age-cohort forecast. Changes flexibility only; national demand held constant.', ['bfs-vehicles-2025', MODEL])
    count = 2100000 * .5 * .5 * .4
    claim('fleet-participants-2035', 'Participating cars in the main example', count, 'cars', 'assumption', 'A subset of the BFE electric fleet with a compatible car, equipped home and willing owner.', '2,100,000 × 50% compatible × 50% equipped × 40% participating = 210,000.', 'All eligibility fractions are illustrative assumptions. Remaining cars continue to draw their driving electricity in fixed demand.')
    claim('fleet-night-connected-2035', 'Cars connected in the evening and overnight window', count * .7, 'cars', 'assumption', 'Illustrative connected homes during local hours 18:00–07:59; the daytime connection fraction is 20%.', '210,000 × 70%.', 'Clock window differs from astronomical day/night. Connection behaviour and trip-level readiness are unvalidated.')
    claim('fleet-working-energy-2035', 'Battery working window across participants', count * 30 / 1e6, 'GWh', 'assumption', 'Total battery-side window between protected 20 kWh and maximum 50 kWh per participant.', '210,000 × (50 − 20) kWh / 1,000,000.', 'Includes disconnected cars; not all energy can reach the grid at once. Driving and conversion losses reduce exportable electricity.')
    claim('fleet-night-v2g-power-2035', 'V2G power ceiling in the overnight window', count * .7 * 7 / 1e6, 'GW', 'assumption', 'Meter-side power ceiling for cars assumed connected at home.', '210,000 × 70% × 7 kW / 1,000,000.', 'Requires energy, matching equipment, connection approval, available household/network capacity and viable voluntary participation. Not firm capacity.')
    for row in rows:
        for field, label, unit in [('unserved_gwh', 'Remaining annual supply', 'GWh'), ('unserved_peak_gw', 'Remaining peak supply', 'GW')]:
            claim('plan-' + row['case']['id'] + '-' + ('energy' if field.endswith('gwh') else 'power'), label + ': ' + row['case']['id'], row[field], unit, 'scenario', 'Additional supply required after the specified generation, storage, imports and vehicle service; hydrological-year weather, 2035 demand.', 'Hourly dispatch: minimize total shortfall, then its peak; annual sum or hourly maximum.', limits)
    screen = [row for row in rows if row['case']['id'] in ['2012-v2g', '2022-v2g', '2024-v2g']]
    claim('plan-procurement-energy', 'Starting winter-energy procurement screen', math.ceil(max(row['winter_shortfall_gwh'] for row in screen) / 100) / 10, 'TWh', 'scenario', 'Largest remaining winter gap among three V2G weather cases, rounded upward to 0.1 TWh.', 'ceil(max(winter shortfall GWh) / 100) / 10.', 'Preliminary market-assessment quantity, conditional on up to 2 GW imports and optimistic dispatch. No statistical reserve margin. Network, further weather/outages, fuel and contracted availability can raise the requirement.')
    claim('plan-procurement-power', 'Starting available-power procurement screen', math.ceil(max(row['unserved_peak_gw'] for row in screen) * 10) / 10, 'GW', 'scenario', 'Largest remaining peak among three V2G weather cases, rounded upward to 0.1 GW.', 'ceil(max(peak shortfall GW) × 10) / 10.', 'Delivered available power, not equipment nameplate or final contracted capacity. Assumes up to 2 GW imports; the no-import case needs more. No statistical reserve margin.')
    inputs = []
    paths = [ROOT / name for name in ['portfolio_model/parameters.json', 'portfolio_model/dispatch.py', 'portfolio_model/run.py', 'portfolio_model/test_model.py', 'portfolio_model/validate.py', 'model/demand.py', 'model/parameters.json', 'data/demand-model-v1/baseline.json', 'hourly_model/load.py', 'hourly_model/dispatch.py', 'hourly_model/prepare.py', 'hourly_model/config.json', 'feasibility_model/run.py', 'feasibility_model/dispatch.py', 'data/hourly-model-v1/weather-hourly.csv.gz', 'data/hourly-model-v1/weather-audit.json', 'data/hourly-model-v1/load-fit.json', 'data/hourly-model-v1/hydro-monthly.json', 'backups/hourly-model-v1-2026-09-10/sources/bfe-monthly-electricity.csv', 'backups/hourly-model-v1-2026-09-10/sources/bfe-monthly-electricity.csv.metadata.json', 'data/processed/nuclear-annual-2016-2025.json']]
    paths += [DATA / name for name in ['method.md', 'source-catalog.json', 'demand-audit.json', 'fleet-trajectory.json', 'case-index.json']]
    for row in rows:
        paths += [DATA / 'cases' / row['case']['id'] / name for name in ['result.json', 'hourly.csv.gz']]
    inputs = [artifact(path) for path in paths]
    write(DATA / 'input-manifest.json', dict(reviewedAt=DATE, files=inputs, provenance='Primary-source catalogue; inherited weather-audit, hydro-monthly and load-fit records; prior website catalogue. All earlier research remains unchanged.'))
    PUBLIC.mkdir(parents=True, exist_ok=True)
    WEB.mkdir(parents=True, exist_ok=True)
    archive = WEB / 'reproduction.zip'
    with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_DEFLATED) as bundle:
        for path in paths + [DATA / 'input-manifest.json']:
            info = zipfile.ZipInfo(str(path.relative_to(ROOT)), date_time=(2026, 9, 18, 0, 0, 0))
            info.compress_type = zipfile.ZIP_DEFLATED
            bundle.writestr(info, path.read_bytes())
    evidence = dict(reviewedAt=DATE, parameters=p, fleet_trajectory=trajectory, cases=rows, claims=claims, inputs=inputs, limits=limits)
    write(WEB / 'evidence.json', evidence)
    files = [artifact(WEB / 'evidence.json', '/downloads/plan-2035/evidence.json'), artifact(archive, '/downloads/plan-2035/reproduction.zip'), artifact(DATA / 'method.md', '/downloads/plan-2035/method.md'), artifact(DATA / 'input-manifest.json', '/downloads/plan-2035/input-manifest.json'), artifact(Path(__file__).resolve(), '/downloads/plan-2035/build_website_portfolio_2035.py')]
    catalog['sources'].append(dict(id=MODEL, title='2035 day/night supply and vehicle-flexibility model', publisher='WattInfo', kind='project-model', url='/downloads/plan-2035/evidence.json', publicationDate=DATE, edition='First 2035 supply package and 12 matched/sensitivity runs', retrievedAt=DATE, reviewedAt=DATE, locator='parameters, fleet_trajectory, cases, partitions, claims; method and complete reproducible hourly bundle', note=limits + ' The 2035 bridge complements the proposed new-nuclear programme. Supply shortfalls identify further procurement needs; no complete funded national plan is claimed.', artifacts=files))
    for collection in ['sources', 'claims']:
        assert catalog[collection][:len(prior[collection])] == prior[collection]
    for path in [WEB / 'catalog.json', ROOT / 'website/src/data/catalog.json', ROOT / 'website/public/downloads/catalog.json']:
        write(path, catalog)
    print(len(claims), 'claims;', len(rows), 'cases;', len(inputs), 'hashed inputs/outputs')


if __name__ == '__main__':
    main()
