import csv
import datetime
import gzip
import hashlib
import json
from collections import defaultdict
from pathlib import Path
from zoneinfo import ZoneInfo

ROOT = Path(__file__).resolve().parents[1]
EDITION = 'website-visual-chapters-2026-09-17'
DATA = ROOT / 'data' / EDITION
ARCHIVE = ROOT / 'backups' / EDITION
REVIEWED = '2026-09-17'


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


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


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


def main():
    DATA.mkdir(parents=True, exist_ok=True)
    catalog = load('data/website-primer-2026-09-16/catalog.json')
    catalog['reviewedAt'] = REVIEWED
    sources = [
        ('kwo-grimselsee-project', 'Grimselsee enlargement: project design', 'Kraftwerke Oberhasli AG (KWO)', 'https://www.grimselstrom.ch/en/projekte/vergroesserung-grimselsee', None, 'Wichtige Speicher; Eckdaten: additional storage, engineering and dated budget', 'Project design, not operating output. The published budget is explicitly a 2019 estimate and is not used as a current quote.', [f'backups/{EDITION}/source-reader.json']),
        ('kwo-grimsel-milestones-2026', 'Grimselsee concession and Grimsel 4 construction start', 'Kraftwerke Oberhasli AG (KWO)', 'https://de.linkedin.com/pulse/grosser-rat-spricht-konzession-f%C3%BCr-grimselseevergr%C3%B6sserung-p7wse', '2026-09-09', 'Opening concession announcement; Bauarbeiten für Grimsel 4 gestartet', 'Operator report: Grimselsee enters the building-permit phase; Grimsel 4 construction officially started 7 July 2026. Separate projects.', [f'backups/{EDITION}/sources/kwo-grimsel-milestones-2026.html', f'backups/{EDITION}/sources/kwo-grimsel-milestones-2026.txt']),
        ('axpo-mattmark-project', 'Mattmark: proposed dam raising', 'Axpo', 'https://www.axpo.com/ch/de/newsroom/medienmitteilungen/2025/staudamm-mattmark-erhoehung-um-zehn-meter-wird-geprueft.html', '2025-01-16', '60 GWh zusätzlicher Winterstrom; Herausforderungen für die Realisierung', 'Dated proposal: about 60 GWh seasonal transfer, unchanged annual generation, CHF 83 million estimate and unresolved concession/environmental/economic conditions. This release does not establish later approval.', [f'backups/{EDITION}/sources/axpo-mattmark-project.html', f'backups/{EDITION}/sources/axpo-mattmark-project.txt']),
        ('kwo-grimsel4-investment', 'Grimsel 4: investment decision and technical specification', 'Kraftwerke Oberhasli AG (KWO), mirrored by VSE', 'https://www.strom.ch/system/files/media/documents/20260327_KWO_Medienmitteilung_Bau_Grimsel_4.pdf', '2026-03-27', 'Pages 1–2: investment, planned works and two 84 MW reversible machines', 'CHF 300 million construction budget, not final expenditure. The original June start target is superseded by the September operator report of a July start.', ['backups/evidence-baseline-2026-09-10/sources/kwo-grimsel4-investment-2026.pdf']),
    ]
    for identifier, title, publisher, url, date, locator, note, paths in sources:
        catalog['sources'].append(dict(id=identifier, title=title, publisher=publisher, kind='primary', url=url, publicationDate=date, edition=f'Published {date}' if date else 'Undated project page; design includes older milestones', retrievedAt=REVIEWED if identifier != 'kwo-grimsel4-investment' else '2026-09-10', reviewedAt=REVIEWED, locator=locator, note=note, artifacts=[artifact(p) for p in paths]))

    rows = list(csv.DictReader((ROOT / 'data/demand-model-v1/annual-components.csv').open()))
    sector_rows = []
    for year in [2024, 2050]:
        totals = defaultdict(float)
        for row in rows:
            if int(row['year']) == year and row['scenario'] == 'central':
                totals[row['sector']] += float(row['electricity_twh'])
        reference = load('data/demand-model-v1/central-2050.json') if year == 2050 else None
        total = sum(totals.values())
        expected = reference['electricity_demand_twh'] if reference else load('data/demand-model-v1/baseline.json')['accounting']['final_consumption_twh']
        assert abs(total - expected) < 1e-8, (year, total, expected)
        sector_rows.append(dict(year=year, total_twh=total, sectors=dict(totals)))
    history = load('data/website-nuclear-history-2026-09-16/history.json')
    nuclear = [dict(year=year, generation_twh=sum(row['net_generation_gwh'] for row in history['annual'] if row['year'] == year) / 1000) for year in range(2016, 2026)]
    household = dict(status='assumption', annual_useful_heat_kwh=12000, boiler_efficiency=0.85, heat_pump_seasonal_performance=3, annual_car_km=12000, fuel_car_input_kwh_per_km=0.6, electric_car_battery_kwh_per_km=0.18, charging_efficiency=0.9)
    household.update(before_heat_kwh=household['annual_useful_heat_kwh'] / household['boiler_efficiency'], after_heat_kwh=household['annual_useful_heat_kwh'] / household['heat_pump_seasonal_performance'], before_car_kwh=household['annual_car_km'] * household['fuel_car_input_kwh_per_km'], after_car_kwh=household['annual_car_km'] * household['electric_car_battery_kwh_per_km'] / household['charging_efficiency'])
    household['before_total_kwh'] = household['before_heat_kwh'] + household['before_car_kwh']
    household['after_total_kwh'] = household['after_heat_kwh'] + household['after_car_kwh']
    household['boundary'] = 'Illustrative assumptions, not a representative Swiss household or a measured equipment comparison. Same useful heat and distance; purchased fuel energy before, purchased electricity after. Excludes other household uses, upstream energy, embodied energy, costs and hourly winter peaks.'
    storage = dict(status='assumption', usable_output_energy_gwh=4, output_options_gw=[0.5, 1, 2], duration_hours=[8, 4, 2], formula='Hours = usable output GWh / constant GW', boundary='Full store, no recharge; energy is already usable at the output, so no extra discharge loss is deducted. No assertion about an actual asset, purchase cost or national demand.')
    hydro = [
        dict(id='grimselsee', name='Grimselsee', region='Bern', service='More stored energy for winter', value=240, unit='GWh', status='Concession granted', status_date='2026-09-09', detail='Additional storage in the proposed enlargement. The building-permit phase comes next.', boundary='Additional stored energy, not an equal increase in annual generation.', sources=['kwo-grimselsee-project', 'kwo-grimsel-milestones-2026']),
        dict(id='mattmark', name='Mattmark', region='Valais', service='Move production into winter', value=60, unit='GWh', status='Proposal under study', status_date='2025-01-16', detail='The operator estimates CHF 83 million. Annual production would stay unchanged.', boundary='Potential seasonal transfer. This dated release does not establish a later construction decision.', sources=['axpo-mattmark-project']),
        dict(id='grimsel4', name='Grimsel 4', region='Bern', service='More power between existing lakes', value=168, unit='MW', status='Construction started', status_date='2026-07-07', detail='Two 84 MW machines; a CHF 300 million investment decision. Works were scheduled to finish in 2032.', boundary='Power and flexibility. No additional reservoir volume or natural generation is credited here.', sources=['kwo-grimsel4-investment', 'kwo-grimsel-milestones-2026']),
    ]
    dataset = dict(reviewedAt=REVIEWED, demand=sector_rows, nuclear=nuclear, household=household, storage=storage, hydro=hydro, methods={'demand':'Sum annual-components.csv electricity_twh by sector for central 2024 and 2050; baseline is an accounting allocation, 2050 is a conditional scenario.', 'nuclear':'Sum four reactors in the BFE-based history each year; divide GWh by 1000. Excludes Mühleberg for all years. Retains the unresolved Gösgen 2025 BFE/operator boundary difference.', 'hydro':'Three distinct projects selected for contrasting services and milestones; not a national total. Stages state the date of supporting evidence.'})
    write(DATA / 'visuals.json', dataset)

    case_path = 'data/system-feasibility-2026-09-10/cases/n8-pv26-2012-battery_none'
    result = load(case_path + '/result.json')
    with gzip.open(ROOT / case_path / 'hourly.csv.gz', 'rt') as handle:
        full = list(csv.DictReader(handle))
    winter = []
    for row in full:
        instant = datetime.datetime.fromisoformat(row['interval_start_utc']).astimezone(ZoneInfo('Europe/Zurich'))
        if instant.month in [10, 11, 12, 1, 2, 3]:
            row['local'] = instant
            winter.append(row)
    candidates = []
    for index, row in enumerate(winter):
        if row['local'].hour != 0 or row['local'].weekday() != 0:
            continue
        end = row['local'] + datetime.timedelta(days=7)
        selected = [item for item in winter[index:index + 169] if item['local'] < end]
        if not selected or selected[-1]['local'] + datetime.timedelta(hours=1) != end:
            continue
        candidates.append((sum(float(item['unserved']) for item in selected), index, len(selected)))
    _, index, length = max(candidates)
    selected = winter[index:index + length]
    assert len(selected) == 168
    week_rows = []
    for row in selected:
        pumping = sum(float(row[key]) for key in row if key.endswith('_charge'))
        pumped = sum(float(row[key]) for key in row if key.endswith('_discharge'))
        demand = float(row['bus_demand_gw'])
        supply = sum(float(row[key]) for key in ['nuclear_gw', 'solar_gw', 'river_gw', 'other_gw', 'natural_release', 'imports']) + pumped
        disposition = demand + pumping + float(row['exports']) + float(row['curtailment']) - float(row['unserved'])
        assert abs(supply - disposition) < 1e-7
        week_rows.append(dict(local_time=row['local'].isoformat(), demand_gw=demand, served_gw=demand - float(row['unserved']), unserved_gw=float(row['unserved']), nuclear_gw=float(row['nuclear_gw']), solar_gw=float(row['solar_gw']), river_gw=float(row['river_gw']), natural_hydro_gw=float(row['natural_release']), pumped_discharge_gw=pumped, pumping_gw=pumping, imports_gw=float(row['imports']), exports_gw=float(row['exports']), curtailment_gw=float(row['curtailment'])))
    week = dict(status='scenario', case_id=result['case']['id'], selection='Complete Monday–Sunday week with the greatest unserved energy among winter weeks in this solved case; 168 hourly intervals. Selected deliberately to illustrate stress, not typical operation.', period='6–12 February, using 2012 weather for hypothetical 2050 demand and capacity', assumptions='8.8 GW hypothetical nuclear with a 90-day winter outage of one 1.1 GW unit; 26 GWp solar; no imports or batteries; finite pumped stores and aggregate natural hydro; perfect foresight. Not a Swiss construction plan or an observed blackout.', boundary='National model electricity bus, including modelled network losses; excludes pumping input from the demand line. Served demand = bus demand − unserved demand. Generation and storage flows are separately provided in the ledger.', unserved_gwh=sum(row['unserved_gw'] for row in week_rows), annual_net_exports_twh=result['exports_gwh'] / 1000, rows=week_rows)
    write(DATA / 'winter-week.json', week)
    with (DATA / 'winter-week.csv').open('w') as handle:
        writer = csv.DictWriter(handle, fieldnames=list(week_rows[0])); writer.writeheader(); writer.writerows(week_rows)
    review = dict(reviewedAt=REVIEWED, concepts=[
        dict(id='hydro-services', sources=['eia-hydropower','kwo-grimselsee-project','axpo-mattmark-project','kwo-grimsel4-investment'], statement='Reservoir enlargement, new natural generation and increased turbine/pump power are different services; selected project numbers must not be added across units.'),
        dict(id='water-economics', sources=['owner-model-v1'], statement='Retaining water sacrifices an alternative sale; a service must earn enough to cover its cost and obligations. Recommended voluntary contracts are editorial proposals.'),
        dict(id='winter-screen', sources=['system-model-v1','visual-chapter-calculations'], statement='Annual exports and unserved hours coexist in this particular hypothetical case. Imports, flexibility and other portfolios have not been ruled out.'),
        dict(id='examples', sources=['visual-chapter-calculations'], statement='Household and storage numbers are teaching assumptions and exact arithmetic, not observations or equipment guarantees.'),
    ])
    write(DATA / 'concept-review.json', review)
    paths = ['visuals.json','winter-week.json','winter-week.csv','concept-review.json']
    model_inputs = ['data/demand-model-v1/annual-components.csv',case_path + '/hourly.csv.gz',case_path + '/result.json','feasibility_model/run.py','feasibility_model/dispatch.py','hourly_model/config.json','hourly_model/dispatch.py']
    assert all((ROOT / p).is_file() for p in model_inputs)
    catalog['sources'].append(dict(id='visual-chapter-calculations', title='Visual explanations: seasonal stress, demand and storage', publisher='WattInfo', kind='project-calculation', url='/downloads/visual-chapters/visuals.json', publicationDate=REVIEWED, edition='Visual chapter data and explicit teaching assumptions', retrievedAt=REVIEWED, reviewedAt=REVIEWED, locator='Named fields and methods; winter-week selection, 168 rows and energy-balance checks', note='Derived from preserved datasets. The selected winter week is modelled, deliberately stressful, and includes hypothetical new reactors. Household and storage examples are assumptions, not observations.', artifacts=[artifact(f'data/{EDITION}/{name}',f'/downloads/visual-chapters/{name}') for name in paths] + [artifact(p, '/downloads/visual-chapters/inputs/' + p.replace('/', '__')) for p in model_inputs] + [artifact('scripts/build_website_visual_chapters.py','/downloads/visual-chapters/build_website_visual_chapters.py')]))
    for identifier, label, value, unit, period, status, definition, limitation, formula, refs in [
        ('grimselsee-extra-storage','Grimselsee: proposed additional stored energy',240,'GWh','Project design reviewed 2026-09-17','scenario','Additional energy storage associated with the proposed lake enlargement.','Not annual new generation or an operating asset.','Operator design estimate; not a WattInfo calculation.',['kwo-grimselsee-project']),
        ('mattmark-winter-shift','Mattmark: proposed winter transfer',60,'GWh','Operator proposal dated 2025-01-16','scenario','Approximate production moved from summer into winter.','Annual generation unchanged; delivery and economics conditional.','Operator project estimate.',['axpo-mattmark-project']),
        ('mattmark-budget','Mattmark: estimated investment',83,'CHF million','Operator estimate dated 2025-01-16','scenario','Indicative investment in the proposed dam raising.','Not a supplier quote or actual outturn.','Operator budget estimate.',['axpo-mattmark-project']),
        ('grimsel4-power','Grimsel 4: planned installed power',168,'MW','Investment decision 2026-03-27','calculated','Two reversible machines of 84 MW each.','Not GWh of stored energy; no separate pump electrical-input rating inferred.','2 × 84 MW',['kwo-grimsel4-investment']),
        ('grimsel4-budget','Grimsel 4: investment decision',300,'CHF million','2026-03-27','scenario','Approximate budget approved by the operator.','Not final construction expenditure.','Operator investment budget.',['kwo-grimsel4-investment']),
        ('example-household-before','Example: purchased fuel before electrification',household['before_total_kwh'],'kWh/year','Illustrative example','calculated','Fuel input for the assumed heating and travel service.',household['boundary'],'12000 / 0.85 + 12000 × 0.6',['visual-chapter-calculations']),
        ('example-household-after','Example: electricity after electrification',household['after_total_kwh'],'kWh/year','Illustrative example','calculated','Electricity input for the same assumed heat and distance.',household['boundary'],'12000 / 3 + 12000 × 0.18 / 0.9',['visual-chapter-calculations']),
        ('winter-week-unserved','Unserved demand in the selected modelled week',week['unserved_gwh'],'GWh','6–12 February, 2012 weather / hypothetical 2050 system','scenario','Hourly unmet model-bus demand summed across the selected stress week.','Not an observed blackout or probability estimate. The case disallows imports and batteries.','Sum 168 hourly unserved GW × 1 hour.',['visual-chapter-calculations','system-model-v1']),
    ]:
        catalog['claims'].append(dict(id=identifier,label=label,value=value,unit=unit,period=period,status=status,definition=definition,limitation=limitation,formula=formula,sources=refs,reviewedAt=REVIEWED))
    for path in [DATA/'catalog.json', ROOT/'website/src/data/catalog.json', ROOT/'website/public/downloads/catalog.json']:
        write(path,catalog)
    print(f'{len(catalog["sources"])} sources, {len(catalog["claims"])} claims; sector totals and 168 hourly balances reconciled.')


if __name__ == '__main__':
    main()
