import csv
import hashlib
import json
import math
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
RESEARCH = ROOT / 'data/cost-metrics-2026-09-17'
EDITION = 'website-costs-2026-09-17'
DATA = ROOT / 'data' / EDITION
REVIEWED = '2026-09-17'


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, ensure_ascii=False, indent=2) + '\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 annuity(rate, years):
    return 1 / years if rate == 0 else rate / (1 - (1 + rate) ** -years)


def checked(actual, expected):
    assert math.isclose(actual, expected, rel_tol=1e-12, abs_tol=1e-8), (actual, expected)


def main():
    DATA.mkdir(parents=True, exist_ok=True)
    catalog = load(ROOT / 'data/website-visual-chapters-2026-09-17/catalog.json')
    catalog['reviewedAt'] = REVIEWED
    indexed = {row['source_id']: row for row in load(RESEARCH / 'source-index.json')['sources']}
    additions = [
        ('eia-lcoe-method-2026', 'Generation cost, storage cost and avoided value', 'April 2026 methodology', 'Pages 1–5: definitions, duty cycle, included costs and avoided value', 'Methods are used here, not US technology prices, tax credits or assumed future improvements.'),
        ('idel-2021-author-preprint', 'Levelized full system costs of electricity: author manuscript', 'February 2021 author manuscript', 'Pages 6–11 and 20–23: technology set, storage, separate annual designs and 95% variant', 'Detailed methodological statements refer to this complete manuscript. It is not verified identical to the final 2022 journal article. The author includes wind-plus-solar and 95%-annual-energy variants; the baseline is not the only case.'),
        ('matsuo-2022-publisher-pdf', 'System costs and the value of power sources', 'Published 2022', 'Section 1.3, page 5: single-source versus mixed-system question; sections 2–3: alternative methods', 'Original methodological critique, with its own allocation choices and Japanese model assumptions. No numerical result is transferred to Switzerland.'),
        ('iea-gec-method-2025', 'Value-adjusted generation cost: IEA model documentation', 'Global Energy and Climate Model, 2025', 'Pages 58–60: VALCOE, energy, capacity and flexibility values', 'VALCOE is a model-based value adjustment, not a complete Swiss grid, risk or consumer-bill calculation.'),
        ('bfe-pv-price-2025', 'Swiss rooftop solar installation prices in 2025', '2025 offers and invoices; report dated 3 August 2026', 'Table 4, page 17: medians, quartiles and sample sizes; pages 7–9: study scope', 'Installer-scope prices excluding VAT, external engineering, roof reinforcement, grid strengthening and batteries. Middle-half ranges are not confidence intervals. The introductory reference to 2024 conflicts with the scope and tables identifying 2025; this edition follows the latter.'),
        ('bfe-market-prices-2026', 'Swiss reference market prices: first two quarters of 2026', 'Publication dated 14 July 2026', 'Pages 2–5: technology-specific quarterly reference prices', 'Measured technology cohorts, not entire fleets. Energy-only historical wholesale benchmarks, excluding VAT; not production costs, retail tariffs or guaranteed plant income.'),
        ('bfe-market-price-method', 'How Swiss reference market prices are calculated', 'Printed update: 1 April 2026', 'Pages 2–4: measured injections, Swiss day-ahead prices and included hydro categories', 'The archived cover says 1 April 2026; an earlier research index recorded 10 April. This edition uses the printed update date. The hydro categories do not represent the entire reservoir and pumped-storage fleet.'),
        ('bfe-lto-2026-background', 'Economics of extending Gösgen and Leibstadt to 80 years', 'Report dated 12 March 2026; publication date not established', 'Summary pages 8–9; Annex B pages 99–108, especially 104–105 on dismantling', 'BFE-commissioned Frontier Economics/Siempelkamp study. Approximate conditional break-even prices based on operator best-estimate investment costs, 7% nominal pretax finance and real 2025 CHF. Scheduled extension assumes sufficient prefunding and no additional dismantling costs; early closure includes continuing costs. No operating licence, new-build quotation or national portfolio ranking is established.'),
    ]
    for identifier, title, edition, locator, note in additions:
        original = indexed[identifier]
        for entry in original['artifacts']:
            actual = artifact(ROOT / entry['path'])
            assert actual['sha256'] == entry['sha256'] and actual['bytes'] == entry['bytes']
        publication = '2026-04-01' if identifier == 'bfe-market-price-method' else original.get('publication_date')
        catalog['sources'].append(dict(id=identifier, title=title, publisher=original['publisher'], kind='primary', url=original['url'], publicationDate=publication, edition=edition, retrievedAt=original['retrieved_at_utc'], reviewedAt=REVIEWED, locator=locator, note=note, artifacts=original['artifacts']))

    observations = load(RESEARCH / 'cost-observations.json')['observations']
    selected = [row for row in observations if row['id'].startswith(('pv-roof-', 'capture-')) or ('-lto-' in row['id'] and not row['id'].endswith('-capex'))]
    for row in selected:
        row['claim_id'] = 'cost-' + row['id']
        for source_date in row['source_dates']:
            if source_date['source_id'] == 'bfe-market-price-method':
                source_date['publication_date'] = '2026-04-01'
        is_extension = '-lto-' in row['id']
        subject = 'Gösgen extension' if row['id'].startswith('kkg-lto-') else 'Leibstadt extension' if row['id'].startswith('kkl-lto-') else row['technology']
        catalog['claims'].append(dict(id=row['claim_id'], label=f"{subject}: {row['metric']} ({row.get('power_band_kwp', row.get('output_basis') or row['reporting_period'])})", value=row['value'], unit=row['unit'], period=row['reporting_period'], status='scenario' if is_extension else 'observed', definition=row['boundary'], limitation='; '.join([row['comparability'], *row['omissions'], row.get('uncertainty') or 'Observed sample or period; not a future guarantee.']), formula='Study discounted-cash-flow break-even price; operator best-estimate extension investment, operating costs and 7% nominal pretax finance. See the original report for its scenario-specific cash flows.' if is_extension else None, sources=row['source_ids'], reviewedAt=REVIEWED))

    finance = [row for row in load(RESEARCH / 'finance-utilization-sensitivities.json') if row['overnight_chf_kw'] == 10000 and row['life_years'] == 60 and row['capacity_factor'] == .9 and row['construction_years'] in [5, 10, 15]]
    assert len(finance) == 12
    for row in finance:
        rate, years = row['real_rate'], row['construction_years']
        factor = sum((1 + rate) ** (years - index - .5) for index in range(years)) / years
        checked(row['finance_factor'], factor)
        checked(row['partial_generation_cost_chf_mwh'], (10000 * factor * annuity(rate, 60) + 120) / (8.76 * .9) + 15)
        row['claim_id'] = f"cost-finance-{round(rate * 100)}-{years}"
        catalog['claims'].append(dict(id=row['claim_id'], label=f"Hypothetical new plant: {round(rate * 100)}% real finance, {years}-year construction", value=row['partial_generation_cost_chf_mwh'], unit='CHF/MWh', period='Illustrative future project; constant CHF assumptions', status='assumption', definition='Partial generation cost for assumed CHF 10,000/kW overnight, 60 operating years and 90% utilization. Same plant and operating allowances in each case.', limitation='Not a Swiss nuclear quote or complete LCOE. Excludes unresolved dismantling/fund costs, full insurance terms, taxes, grid and backup. Longer construction excludes additional site overhead and replacement supply.', formula='Commissioning capital = overnight cost × mean((1+r)^(T-k-0.5), k=0…T-1). Cost = (commissioning capital × CRF(r,60) + CHF 120/kW-year) / (8.76 × 0.9) + CHF 15/MWh fuel/variable/waste allowance.', sources=['cost-chapter-calculations'], reviewedAt=REVIEWED))

    storage = [row for row in load(RESEARCH / 'storage-duration-utilization-sensitivities.json') if row['duration_hours'] == 4 and row['life_years'] == 15 and row['real_rate'] == .07 and row['charge_price_chf_mwh'] == 40]
    assert len(storage) == 4
    for row in storage:
        checked(row['annual_discharge_mwh'], 4 * row['full_cycles_year'])
        checked(row['lcos_including_charging_chf_mwh'], 1500000 * (annuity(.07, 15) + .04) / row['annual_discharge_mwh'] + 40 / .85)
        assert row['chronologically_possible_at_same_charge_power'] and row['minimum_charge_discharge_hours'] <= 8760
        row['claim_id'] = f"cost-storage-{row['full_cycles_year']}"
        catalog['claims'].append(dict(id=row['claim_id'], label=f"Illustrative battery cost at {row['full_cycles_year']} full cycles per year", value=row['lcos_including_charging_chf_mwh'], unit='CHF/MWh discharged', period='Assumed 15-year service; constant CHF', status='assumption', definition='1 MW / 4 MWh usable AC output; CHF 1.5 million assumed purchase cost, 7% real finance, 85% round-trip efficiency and CHF 40/MWh charging electricity.', limitation='No supplier quote or revenue guarantee. Annual 4% cost allowance already includes augmentation. Excludes unpriced site/grid, tax and insurance items. Rare-use capacity or reserve value is separate from energy cost.', formula='LCOS = 1,500,000 × (CRF(7%,15) + 4%) / (4 × annual full cycles) + 40 / 0.85.', sources=['cost-chapter-calculations'], reviewedAt=REVIEWED))

    dataset = dict(reviewedAt=REVIEWED, observations=selected, finance=finance, storage=storage, methods=dict(finance='Even construction expenditure at annual midpoints, capitalized to commissioning; unchanged physical plant and no future learning. Construction duration and idle delay are different experiments.', storage='Selected four-hour cases include charging once. A necessary charging/discharging time-budget check passes; suitable market opportunities are not established.', observations='Original currencies and price years retained. No cross-technology total-cost ranking or inflation conversion.'), review_notes=[dict(source_id='bfe-market-price-method', field='printed_update_date', original_research_value='2026-04-10', reviewed_value='2026-04-01', locator='Archived PDF cover, page 1; earlier research remains unchanged.')])
    write(DATA / 'charts.json', dataset)
    for name, rows in [('finance', finance), ('storage', storage)]:
        with (DATA / f'{name}.csv').open('w', newline='') as handle:
            writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
            writer.writeheader()
            writer.writerows(rows)
    concepts = dict(reviewedAt=REVIEWED, concepts=[
        dict(id='cost-boundaries', statement='Generation cost, storage cost, market value, owner cash flow and the customer bill answer different questions.', sources=['eia-lcoe-method-2026', 'iea-gec-method-2025']),
        dict(id='lfscoe-scope', statement='The inspected 2021 LFSCOE baseline tests restricted technology sets plus storage. Its mixed and 95%-annual-energy variants are material qualifications. A Swiss portfolio comparison needs the actual allowed mix and a common reliability requirement.', sources=['idel-2021-author-preprint', 'matsuo-2022-publisher-pdf']),
        dict(id='double-counting', statement='Charging energy, construction finance, fund contributions and reserve service payments require consistent accounting boundaries. Transfers change payers and can alter finance; they do not automatically eliminate underlying resource expenditure.', sources=['cost-chapter-calculations']),
        dict(id='extension-decision', statement='Continuing an existing plant and building a new one are distinct investments. The extension results are conditional study scenarios, not authorizations.', sources=['bfe-lto-2026-background']),
    ])
    write(DATA / 'concept-review.json', concepts)
    artifacts = [artifact(DATA / name, '/downloads/costs/' + name) for name in ['charts.json', 'finance.csv', 'storage.csv', 'concept-review.json']]
    for name in ['metrics.json', 'cost-ledger.json', 'assumptions.json', 'comparison-contract.json', 'double-counting-checks.json']:
        path = RESEARCH / name
        if path.exists():
            artifacts.append(artifact(path, '/downloads/costs/' + name))
    for name in ['source-index.json', 'cost-observations.json', 'requirements.txt']:
        artifacts.append(artifact(RESEARCH / name, '/downloads/costs/research-' + name))
    for name in ['finance-utilization-sensitivities.json', 'storage-duration-utilization-sensitivities.json', 'claim-catalog.json']:
        artifacts.append(artifact(RESEARCH / name))
    artifacts.append(artifact(ROOT / 'scripts/calculate_cost_metrics.py', '/downloads/costs/calculate_cost_metrics.py'))
    artifacts.append(artifact(ROOT / 'scripts/build_website_costs.py', '/downloads/costs/build_website_costs.py'))
    catalog['sources'].append(dict(id='cost-chapter-calculations', title='Cost comparisons: inputs, calculations and boundaries', publisher='WattInfo', kind='project-calculation', url='/downloads/costs/charts.json', publicationDate=REVIEWED, edition='Curated from the cost-metrics research package, 17 September 2026', retrievedAt=REVIEWED, reviewedAt=REVIEWED, locator='Named observation IDs; 12 finance cases, four battery-use cases; formulas and input fields', note='Observed prices, commissioned study scenarios and explicit analyst assumptions remain distinct. The research source index resolves identifiers used in the full methods and assumption downloads. Calculation scripts expect the research repository layout; selected inputs and formulas are also supplied directly. This package does not establish a least-cost Swiss portfolio, procurement price, consumer tariff or guaranteed investment return.', artifacts=artifacts))
    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; 12 finance and four storage cases independently reconciled.')


if __name__ == '__main__':
    main()
