#!/usr/bin/env python3
"""Reproduce the fictional migration/service example with the Python standard library.

All positions, travel times, capacities, and arrivals are synthetic.
Coordinates are in a fictional local kilometre grid, not a geographic CRS.
Run from anywhere; outputs are written relative to this script's package.
"""

from __future__ import annotations

import heapq
import json
import math
from html import escape
from pathlib import Path

CONTENT = Path(__file__).resolve().parent
DATA = CONTENT / 'data'
ASSETS = CONTENT / 'assets'

def shortest_times(nodes, edges, source, season):
    """Undirected shortest paths in minutes; disconnected nodes remain None."""
    graph = {n['id']: [] for n in nodes}
    for e in edges:
        weight = e[season + '_minutes']
        if weight is None:
            continue
        if weight < 0:
            raise ValueError('Travel times must be non-negative')
        graph[e['from']].append((e['to'], weight))
        graph[e['to']].append((e['from'], weight))
    dist = {k: math.inf for k in graph}
    dist[source] = 0
    queue = [(0, source)]
    while queue:
        value, node = heapq.heappop(queue)
        if value != dist[node]:
            continue
        for neighbour, weight in graph[node]:
            new = value + weight
            if new < dist[neighbour]:
                dist[neighbour] = new
                heapq.heappush(queue, (new, neighbour))
    return {k: None if not math.isfinite(v) else v for k, v in dist.items()}

def calculate(example):
    params = example['parameters']
    travel = {s: shortest_times(example['nodes'], example['edges'], 'clinic', s)
              for s in ['dry', 'wet']}
    rows = []
    for node in example['nodes']:
        if node['kind'] != 'settlement':
            continue
        base = node['population_initial'] * (1 + params['background_growth_fraction'])
        q = params['water_litres_per_person_day']
        capacity = node['water_capacity_litres_day']
        background_gap = max(0, base * q - capacity)
        for scenario in ['lower', 'central', 'higher']:
            arrivals = node['additional_residents'][scenario]
            pop = base + arrivals
            demand = pop * q
            gap = max(0, demand - capacity)
            rows.append({
                'id': node['id'], 'name': node['name'], 'scenario': scenario,
                'population_without_project': round(base, 6),
                'additional_residents': arrivals,
                'population_with_project': round(pop, 6),
                'demand_litres_day': round(demand, 6),
                'capacity_litres_day': capacity,
                'background_gap_litres_day': round(background_gap, 6),
                'gap_litres_day': round(gap, 6),
                'gap_increase_litres_day': round(gap - background_gap, 6),
                'gap_percent_capacity': round(100 * gap / capacity, 6),
                'dry_clinic_minutes': travel['dry'][node['id']],
                'wet_clinic_minutes': travel['wet'][node['id']],
            })
    return rows

def svg_open(width, height, title, description):
    return [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">',
            f'<title id="title">{escape(title)}</title><desc id="desc">{escape(description)}</desc>',
            '<style>text{font-family:Arial,sans-serif;fill:#172b35}.heading{font-size:21px;font-weight:700}.sub{font-size:13px}.label{font-size:13px;font-weight:700}.small{font-size:11px}.value{font-size:12px}</style>',
            f'<rect width="{width}" height="{height}" fill="#fff"/>']

def text_svg(x, y, value, cls='small', anchor='start'):
    return f'<text x="{x:.1f}" y="{y:.1f}" class="{cls}" text-anchor="{anchor}">{escape(str(value))}</text>'

def map_portfolio(example, rows):
    selected = {r['id']: r for r in rows if r['scenario'] == 'central'}
    by_id = {n['id']: n for n in example['nodes']}
    out = svg_open(1020, 575, 'Three different spatial priorities',
        'Synthetic local-grid maps. Junction receives 1000 additional residents, Gate 600, Riverside 240, Hill 60. Water deficits relative to delivered capacity are 4.8, 27.7, 36, and zero percent respectively. Wet-season travel to the clinic is 6, 24, 56, and 66 minutes respectively. Geometry and values are fictional.')
    out += [text_svg(24, 34, 'Three different spatial priorities', 'heading'),
            text_svg(24, 58, 'Fictional local kilometre grid • central scenario • no real settlement is represented', 'sub')]
    titles = ['Additional residents', 'Water deficit / capacity', 'Wet-season clinic access']
    for panel in range(3):
        left = 24 + panel * 332
        def pos(n):
            return left + 12 + n['x_km'] * 27, 405 - n['y_km'] * 30
        out.append(f'<rect x="{left}" y="82" width="316" height="366" rx="7" fill="#f5f8f8" stroke="#dae3e5"/>')
        out.append(text_svg(left + 12, 107, titles[panel], 'label'))
        # Local coordinates, not a georeferenced basemap.
        for e in example['edges']:
            a, b = pos(by_id[e['from']]), pos(by_id[e['to']])
            out.append(f'<path d="M{a[0]},{a[1]} L{b[0]},{b[1]}" stroke="#a1b3b9" stroke-width="2.3" fill="none"/>')
        # A symbolic river near the crossing.
        out.append(f'<path d="M{left+237},142 Q{left+216},246 {left+235},394" stroke="#83bfcb" stroke-width="4" fill="none" stroke-dasharray="6 3"/>')
        for node in example['nodes']:
            x, y = pos(node)
            if node['kind'] == 'settlement':
                row = selected[node['id']]
                if panel == 0:
                    radius = 5 + math.sqrt(row['additional_residents']) * .48
                    fill = '#276d79'
                    value = f"+{row['additional_residents']:,}"
                elif panel == 1:
                    radius = 11
                    fill = '#bf5b18' if row['gap_litres_day'] > 0 else '#fff'
                    value = f"{row['gap_percent_capacity']:.1f}%"
                else:
                    radius = 11
                    fill = '#bf5b18' if row['wet_clinic_minutes'] > 30 else '#27717b'
                    value = f"{row['wet_clinic_minutes']} min"
                out.append(f'<circle cx="{x}" cy="{y}" r="{radius:.1f}" fill="{fill}" fill-opacity=".88" stroke="#153e49" stroke-width="1.4"/>')
                # Individual placement prevents collisions with clinic, project, and river.
                offsets = {'hill':(-18,-35,'start'), 'riverside':(-16,-20,'end'),
                           'junction':(-14,28,'end'), 'gate':(-10,30,'end')}
                dx, dy, anchor = offsets[node['id']]
                out.append(text_svg(x+dx,y+dy,node['name'],'label',anchor))
                out.append(text_svg(x+dx,y+dy+15,value,'value',anchor))
            elif node['kind'] == 'clinic':
                out.append(f'<rect x="{x-6}" y="{y-6}" width="12" height="12" fill="#fff" stroke="#153e49" stroke-width="1.4"/>')
                out.append(text_svg(x-13,y-8,'Clinic','small','end'))
            elif node['kind'] == 'project':
                out.append(f'<path d="M{x},{y-8} L{x+8},{y+7} L{x-8},{y+7} Z" fill="#425363"/>')
                out.append(text_svg(x,y-14,'Project','small','middle'))
            else:
                out.append(f'<circle cx="{x}" cy="{y}" r="3" fill="#425363"/>')
                out.append(text_svg(x+5,y+15,node['name'],'small'))
        notes = [
            ['Circle size follows arrivals; labels give counts.', 'Junction receives the most additional residents.'],
            ['Shortfall divided by delivered capacity.', 'Riverside has the largest proportional deficit.'],
            ['Route times are invented network costs.', 'Hill and Riverside exceed the illustrative 30 min test.']
        ][panel]
        out.append(text_svg(left+5,472,notes[0],'small'))
        out.append(text_svg(left+5,492,notes[1],'small'))
    out += [text_svg(24, 531, 'All maps show the same schematic geometry. Colours supplement the labels; they do not define migrant risk.', 'sub'),
            text_svg(24, 553, 'Source: synthetic-example.json; calculations: reproduce_example.py. No empirical forecast or technical design threshold.', 'small'), '</svg>']
    return '\n'.join(out)

def deficit_chart(rows):
    central = [r for r in rows if r['scenario'] == 'central']
    out = svg_open(940, 412, 'Separate existing shortages from additional demand',
        'Synthetic daily water deficits. Gate has a 14400 litre additional deficit. Junction has a 9600 litre additional deficit. Riverside has an existing 1920 litre deficit plus a 9600 litre increase, totalling 11520 litres. Hill has no deficit. Sum of local deficits is 35520 litres; pooled regional shortfall is 28800 litres.')
    out += [text_svg(24,34,'Separate existing shortages from additional demand','heading'),
            text_svg(24,59,'Fictional central scenario • daily water shortfall • units: litres per day','sub')]
    for i,r in enumerate(central):
        y=98+i*51
        x0=138
        scale=.042
        existing=r['background_gap_litres_day']*scale
        added=r['gap_increase_litres_day']*scale
        out.append(text_svg(24,y+18,r['name'],'label'))
        if existing:
            out.append(f'<rect x="{x0}" y="{y}" width="{existing}" height="25" fill="#bc5c1d"/>')
        if added:
            out.append(f'<rect x="{x0+existing}" y="{y}" width="{added}" height="25" fill="#276d79"/>')
        out.append(text_svg(x0+existing+added+12,y+18,f"{r['gap_litres_day']:,.0f}",'value'))
    out += [f'<rect x="24" y="322" width="14" height="14" fill="#bc5c1d"/>',text_svg(45,334,'Shortfall without project','small'),
            f'<rect x="268" y="322" width="14" height="14" fill="#276d79"/>',text_svg(289,334,'Increase in shortfall in central scenario','small'),
            text_svg(24,366,'Local deficits total 35,520 L/day. Regional pooling gives 28,800 L/day, but assumes capacity can be transferred.','sub'),
            text_svg(24,391,'All inputs are synthetic. This calculation does not establish water adequacy, quality, affordability, or engineering capacity.','small'),'</svg>']
    return '\n'.join(out)

def main():
    example=json.loads((DATA/'synthetic-example.json').read_text())
    rows=calculate(example)
    # Verify the published figures and a genuinely important spatial distinction.
    central={r['id']:r for r in rows if r['scenario']=='central'}
    assert central['gate']['gap_litres_day']==14400
    assert central['junction']['gap_litres_day']==9600
    assert central['riverside']['gap_litres_day']==11520
    assert central['riverside']['background_gap_litres_day']==1920
    assert central['hill']['wet_clinic_minutes']==66
    assert central['riverside']['wet_clinic_minutes']==56
    assert sum(r['gap_litres_day'] for r in central.values())==35520
    pooled=max(0,sum(r['demand_litres_day'] for r in central.values())-sum(r['capacity_litres_day'] for r in central.values()))
    assert pooled==28800
    results={'synthetic':True,'rows':rows,'central_summary':{
        'additional_residents':sum(r['additional_residents'] for r in central.values()),
        'sum_local_gap_litres_day':35520,'pooled_gap_litres_day':pooled,
        'note':'Pooled capacity is not assumed transferable in the local calculations.'}}
    DATA.mkdir(exist_ok=True,parents=True); ASSETS.mkdir(exist_ok=True,parents=True)
    (DATA/'synthetic-results.json').write_text(json.dumps(results,indent=2)+'\n')
    (ASSETS/'spatial-priorities.svg').write_text(map_portfolio(example,rows))
    (ASSETS/'water-shortfalls.svg').write_text(deficit_chart(rows))
    print(json.dumps({'verified':True,'settlements':len(central),'scenario_records':len(rows),'central_summary':results['central_summary']},indent=2))

if __name__=='__main__':
    main()
