import copy
import csv
from datetime import datetime, timedelta
import gzip
import hashlib
import json
import math
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / 'data/website-reliability-2026-09-18'
REVIEWED = '2026-09-18'
MODEL = 'reliability-plan-calculations'
CASE = ROOT / 'data/system-feasibility-2026-09-10/cases/n8-pv26-2012-battery_none'


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


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


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


def main():
    prior = load(ROOT / 'data/website-eth-psi-2026-09-18/catalog.json')
    catalog = copy.deepcopy(prior)
    titles = {
        'reserve-financing-2026': 'Federal Council: reserve-plant financing proposal, June 2026',
        'reserve-credit-message-2026': 'Reserve financing: cost boundaries and fuel obligations',
        'reserve-parliament-2026': 'Parliament: consideration of the main reserve credit suspended',
    }
    notes = {
        'reserve-financing-2026': 'A financing proposal and contracted future capacity, not commissioned plants or a final spending outturn. The newer parliamentary record supplies the subsequent deliberation status.',
        'reserve-credit-message-2026': 'Provisional message accompanying the 12 June release. Sections 3.2.2–3.2.3, 3.2.10, 3.2.13 and 4.3 reviewed. CHF 2.18 billion includes CHF 300 million planning contingency; activation fuel and balancing costs are excluded. These contracted-service costs are not a generic technology price or a 2050 national reserve budget.',
        'reserve-parliament-2026': 'Reader extract, not original HTML; direct request returned 403. The 11 August committee release suspends the main CHF 2.18 billion credit pending the next ElCom report, while treating two additional credits separately. Status checked 18 September against a record updated 11 September 2026.',
    }
    for row in load(DATA / 'source-catalog.json'):
        raw = artifact(ROOT / row['path'])
        assert raw['sha256'] == row['sha256'] and raw['bytes'] == row['bytes']
        files = [raw]
        if row.get('textPath'):
            files.append(artifact(ROOT / row['textPath']))
        catalog['sources'].append(dict(id=row['id'], title=titles[row['id']], publisher=row['publisher'], kind='primary', url=row['url'], publicationDate=row['publicationDate'], edition=row['publicationDate'], retrievedAt=row['retrievedAt'], reviewedAt=REVIEWED, locator=row['locator'], note=notes[row['id']], artifacts=files))
    beznau = load(ROOT / 'data/beznau-extension-2026-09-18/source-catalog.json')
    for row in beznau['sources']:
        if row['id'] not in ['ensi-operating-horizon', 'axpo-decision-faq']:
            continue
        raw = artifact(ROOT / row['path'])
        assert raw['sha256'] == row['sha256']
        files = [raw]
        if row.get('text_path'):
            files.append(artifact(ROOT / row['text_path']))
        catalog['sources'].append(dict(id='beznau-' + row['id'], title='Beznau: ' + ('licence duration and safety review' if row['id'].startswith('ensi') else 'owner decision and longer-operation FAQ'), publisher=row['publisher'], kind='primary', url=row['url'], publicationDate=row['publication_date'], edition=row['publication_date'], retrievedAt=row['retrieved_at_utc'], reviewedAt=REVIEWED, locator=row['locator'], note='Unlimited licence duration does not establish safety or owner commitment for additional years. Axpo says it rejected operation beyond 2033. ENSI content is an archived reader extract; the owner FAQ is original HTML.', artifacts=files))

    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='Reliability plan reviewed 18 September 2026', definition=definition, formula=formula, limitation=limitation, sources=sources or [MODEL], reviewedAt=REVIEWED)
        claims.append(item)
        catalog['claims'].append(item)
        return id

    bridge = load(ROOT / 'data/nuclear-delivery-2026-09-17/existing-fleet-bridge.json')
    delivery = load(ROOT / 'data/website-nuclear-delivery-2026-09-17/evidence.json')
    years = []
    for year in range(2032, 2054):
        missing = sum(bridge['mean_2021_2025_twh'][plant] for plant in ['beznau-1', 'beznau-2'] if year > delivery['retirement_intentions'][plant])
        years.append(dict(year=year, historical_output_to_replace_twh=missing))
    timelines = []
    for case in delivery['schedules']:
        start = case['commercial_year']
        energy = sum(row['historical_output_to_replace_twh'] for row in years if row['year'] < start)
        id = claim('bridge-energy-' + str(start), 'Replacement-energy scale before first operation in ' + str(start), energy, 'TWh', 'scenario', 'Cumulative historical-output equivalent from full years after the announced Beznau retirements to the year before first new commercial operation.', 'Sum Beznau unit 2 mean from 2033 and unit 1 mean from 2034 through ' + str(start - 1) + '.', 'Holds 2021–2025 mean generation constant for an illustration. Excludes retirement-year partial effects, demand growth, losses and winter adequacy. Does not assume a new unit replaces the entire gap.', [MODEL, 'nuclear-delivery-calculations'])
        timelines.append(dict(label=case['label'], commercial_year=start, cumulative_twh=energy, claim_id=id))

    result = load(CASE / 'result.json')
    with gzip.open(CASE / 'hourly.csv.gz', 'rt') as handle:
        original = list(csv.DictReader(handle))
    assert len(original) == result['audit']['hours'] == 8784
    hourly = []
    for index, row in enumerate(original):
        instant = datetime.fromisoformat(row['interval_start_utc'])
        if index:
            assert instant - datetime.fromisoformat(original[index - 1]['interval_start_utc']) == timedelta(hours=1)
        pumping = sum(float(row[key]) for key in row if key.endswith('_charge'))
        discharge = sum(float(row[key]) for key in row if key.endswith('_discharge'))
        supply = sum(float(row[key]) for key in ['nuclear_gw', 'solar_gw', 'river_gw', 'other_gw', 'natural_release', 'imports']) + discharge
        disposition = float(row['bus_demand_gw']) + pumping + float(row['exports']) + float(row['curtailment']) - float(row['unserved'])
        assert abs(supply - disposition) < 1e-7
        gap = float(row['unserved'])
        assert math.isfinite(gap) and gap >= -1e-8
        hourly.append(dict(interval_start_utc=row['interval_start_utc'], bus_demand_gw=float(row['bus_demand_gw']), gap_gw=max(0, gap)))
    total = sum(row['gap_gw'] for row in hourly)
    peak = max(row['gap_gw'] for row in hourly)
    assert abs(total - result['unserved_gwh']) < 1e-7
    assert abs(peak - result['unserved_peak_gw']) < 1e-7
    reserves = []
    for power in [0, 1, 2, 4, math.ceil(peak * 100) / 100]:
        supplied = sum(min(row['gap_gw'], power) for row in hourly)
        remaining = total - supplied
        hours = sum(row['gap_gw'] > power + 1e-7 for row in hourly)
        reserves.append(dict(available_gw=power, supplied_gwh=supplied, remaining_gwh=max(0, remaining), shortage_hours=hours, residual_peak_gw=max(0, peak - power), output_equivalent_hours=supplied / power if power else None))
    reserve_limit = 'Post-dispatch overlay on one hypothetical 2050 case using 2011/12 weather. All reserve power is assumed available in each shortage hour, with unlimited activation energy. Existing hydro dispatch is held fixed. No start/ramp constraints, reserve outages, fuel logistics, internal grid or redispatch; no statistical reliability or optimal reserve-size claim.'
    claim('reserve-stress-peak', 'Peak shortfall in the nuclear-inclusive stress case', peak, 'GW', 'scenario', 'Largest hourly electricity shortfall after the archived generation and storage dispatch.', 'Maximum hourly unserved GW across 8,784 hours.', reserve_limit)
    claim('reserve-stress-hours', 'Hours with a shortfall', result['shortage_hours'], 'hours', 'scenario', 'Modelled hours with unserved power above numerical tolerance.', 'Count hourly gaps greater than 0.000001 GW.', reserve_limit)
    claim('reserve-overlay-power', 'Available reserve that covers this frozen shortfall', reserves[-1]['available_gw'], 'GW', 'assumption', 'The observed model peak rounded upward to 0.01 GW for the overlay.', 'ceil(model peak GW × 100) / 100.', reserve_limit)
    claim('reserve-overlay-full-load', 'Output-equivalent hours for the reserve overlay', reserves[-1]['output_equivalent_hours'], 'hours', 'calculated', 'Annual reserve energy divided by assumed available reserve power. Different from hours with any operation.', '216.685430632421 GWh / ' + str(reserves[-1]['available_gw']) + ' GW.', reserve_limit)
    for id, label, value, unit, definition, formula, source in [
        ('reserve-contracted-power', 'Planned reserve capacity in the federal proposal', 583, 'MW', 'Aggregate future contracted capacity described in the June 2026 proposal.', 'Published programme total.', 'reserve-financing-2026'),
        ('reserve-contract-years', 'Planned reserve availability period', 15, 'years', 'Proposed availability period starting around 2030, subject to project delivery.', 'Published contractual duration.', 'reserve-financing-2026'),
        ('reserve-cost-contracts', 'Reserve construction and availability cost', 1880, 'CHF million', 'Aggregate construction and 15-year availability estimate before planning contingency.', 'Section 3.2.3 published total.', 'reserve-credit-message-2026'),
        ('reserve-cost-contingency', 'Planning contingency in the main reserve credit', 300, 'CHF million', 'Allowance included for specified project cost increases.', 'Section 3.2.3 published allowance.', 'reserve-credit-message-2026'),
        ('reserve-cost-credit', 'Main reserve credit requested', 2180, 'CHF million', 'Construction and availability including contingency and VAT; excludes activation fuel and energy-balancing costs.', '1,880 + 300 = 2,180.', 'reserve-credit-message-2026'),
        ('reserve-cost-preparation', 'Separate preparation credit requested', 100, 'CHF million', 'Separate credit bringing forward project work and component payments; the message says this does not increase total costs.', 'Section 3.2.2 and the parliamentary record.', 'reserve-credit-message-2026'),
        ('reserve-cost-monthey', 'Separate Monthey transition credit requested', 31.3, 'CHF million', 'Separate transition credit bringing forward work originally planned under the long-term contract, with lower subsequent conversion costs.', 'Section 3.2.2 and the parliamentary record.', 'reserve-credit-message-2026'),
        ('reserve-fuel-days', 'Contractual on-site fuel duration', 16, 'days', 'Fuel-storage obligation described in the federal message.', 'Section 3.2.10.', 'reserve-credit-message-2026'),
        ('reserve-fuel-logistics', 'Contractual fuel-resupply logistics horizon', 10, 'weeks', 'Obligation to secure resupply and logistics; not ten weeks of on-site stock.', 'Section 3.2.10.', 'reserve-credit-message-2026'),
    ]:
        claim(id, label, value, unit, 'scenario', definition, formula, 'Dated proposal/contractual description. The committee suspended consideration of the main credit on 11 August 2026. Values are not current commissioned supply, final outturn or generic supplier prices.', [source, MODEL])

    inputs = [artifact(path) for path in [CASE / 'hourly.csv.gz', CASE / 'result.json', ROOT / 'data/nuclear-delivery-2026-09-17/existing-fleet-bridge.json', ROOT / 'data/website-nuclear-delivery-2026-09-17/evidence.json', ROOT / 'data/website-visual-chapters-2026-09-17/winter-week.json', DATA / 'source-catalog.json', ROOT / 'data/beznau-extension-2026-09-18/source-catalog.json']]
    dataset = dict(reviewedAt=REVIEWED, bridge=dict(retirement_convention='Each unit contributes its historical mean through its announced final operating year; full replacement begins in the following year. Partial final-year effects are excluded.', annual=years, timelines=timelines, mean_twh=bridge['beznau_combined_mean_twh']), reserve=dict(case_id=result['case']['id'], period='October 2011–September 2012 weather, hypothetical 2050 demand and fleet', nuclear_gw=8.8, solar_gwp=26, unserved_gwh=total, peak_gw=peak, shortage_hours=result['shortage_hours'], rows=reserves, limitation=reserve_limit, base_assumptions='One 1.1 GW nuclear unit unavailable for 90 winter days; scheduled maintenance and 98% other availability; no imports or batteries; finite pumped stores; pooled natural hydro; perfect year-ahead foresight; no internal grid. The hypothetical eight-unit fleet has no demonstrated delivery plan.'), financing=dict(contract_chf_million=1880, contingency_chf_million=300, main_credit_chf_million=2180, preparation_credit_chf_million=100, monthey_credit_chf_million=31.3, capacity_mw=583, years=15, status='Main-credit committee consideration suspended on 11 August 2026 pending further ElCom evidence; record checked 18 September 2026.', excluded='Activation fuel and energy-balancing costs; not a 2050 reserve budget.'), claims=claims, inputs=inputs)
    write(DATA / 'evidence.json', dataset)
    with (DATA / 'reserve-hourly.csv').open('w', newline='') as handle:
        writer = csv.DictWriter(handle, fieldnames=list(hourly[0]), lineterminator='\n')
        writer.writeheader()
        writer.writerows(hourly)
    files = [artifact(DATA / 'evidence.json', '/downloads/reliability/evidence.json'), artifact(DATA / 'reserve-hourly.csv', '/downloads/reliability/reserve-hourly.csv'), artifact(Path(__file__).resolve(), '/downloads/reliability/build_website_reliability.py')]
    files += [artifact(CASE / name, '/downloads/reliability/inputs/' + name) for name in ['hourly.csv.gz', 'result.json']]
    catalog['sources'].append(dict(id=MODEL, title='Beznau replacement timeline and reserve-supply arithmetic', publisher='WattInfo', kind='project-calculation', url='/downloads/reliability/evidence.json', publicationDate=REVIEWED, edition='First replacement-and-reserve assessment', retrievedAt=REVIEWED, reviewedAt=REVIEWED, locator='bridge; reserve; financing; claims; inputs; complete hourly CSV', note='Historical replacement scale and an assumed reserve overlay on an existing hypothetical nuclear-inclusive stress case. Preserves the original model and its limitations. No national cost optimum, fleet delivery forecast or probabilistic blackout estimate.', artifacts=files))
    for collection in ['sources', 'claims']:
        assert catalog[collection][:len(prior[collection])] == prior[collection]
    for path in [DATA / 'catalog.json', ROOT / 'website/src/data/catalog.json', ROOT / 'website/public/downloads/catalog.json']:
        write(path, catalog)
    print(f'Added {len(claims)} claims. Peak {peak:.3f} GW; {total:.3f} GWh across {result["shortage_hours"]} hours.')


if __name__ == '__main__':
    main()
