"""SafeEvac AI — cPanel/Passenger WSGI build.

Framework-free WSGI application so the hackathon demo can run on a standard
cPanel Python Application without a separate Streamlit process or exposed port.
"""
from __future__ import annotations

import base64
import csv
import html
import io
import json
import os
from functools import lru_cache
from pathlib import Path
from urllib.parse import parse_qs, urlencode

import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

from safeevac_model import (
    FEATURES,
    predict_with_uncertainty,
    recommended_interventions,
    risk_band,
    train_model,
    validate_scenario,
)

APP_DIR = Path(__file__).resolve().parent
TITANIC_PATH = APP_DIR / "legacy_titanic" / "train.csv"

SECTORS = ["Office", "Care", "Education", "Hospitality", "Retail", "Logistics", "Industry"]
HAZARDS = ["Fire", "Smoke", "Power outage", "Chemical", "Flooding"]

DEFAULT_SCENARIO = {
    "sector": "Office", "hazard": "Fire", "occupant_count": 250, "floors": 4,
    "exits_total": 3, "blocked_exits": 0, "stair_width_m": 1.4, "avg_route_m": 55.0,
    "bhv_count": 6, "mobility_support_count": 5, "sensory_cognitive_support_count": 4,
    "visitor_pct": 20.0, "alarm_delay_s": 75, "trained_occupants_pct": 65.0,
    "communication_channels": 2, "night_shift": 0, "drill_recent": 1,
}

CSS = r"""
:root{--bg:#070c12;--panel:#0d151e;--panel2:#111c27;--line:#203141;--text:#f5f8fb;--muted:#8ea0b2;--blue:#18b5ff;--blue2:#195cff;--green:#40d56b;--orange:#ff9f1a;--red:#ff4d3d;--shadow:0 18px 50px rgba(0,0,0,.35)}
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 70% 10%,#102131 0,#070c12 38%,#05080c 100%);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;min-height:100vh}
a{color:inherit;text-decoration:none}.shell{display:grid;grid-template-columns:220px minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;border-right:1px solid var(--line);background:linear-gradient(180deg,rgba(10,19,29,.98),rgba(6,11,17,.98));padding:26px 18px}.brand{display:flex;align-items:center;gap:10px;font-size:20px;font-weight:800;margin:0 8px 32px}.shield{width:31px;height:31px;border:1.5px solid #d8f4ff;border-radius:10px;display:grid;place-items:center;color:var(--blue);box-shadow:0 0 25px rgba(24,181,255,.14)}.nav{display:grid;gap:7px}.nav a{padding:12px 13px;border:1px solid transparent;border-radius:8px;color:#aebbc8;font-size:14px;display:flex;gap:10px;align-items:center}.nav a:hover,.nav a.active{color:white;background:linear-gradient(90deg,rgba(20,102,187,.45),rgba(12,58,101,.25));border-color:#174c75;box-shadow:inset 3px 0 0 var(--blue)}.nav .ico{width:18px;text-align:center;color:#c5d0da}.ai-pill{position:absolute;bottom:28px;left:26px;border:1px solid #22384b;border-radius:20px;padding:7px 12px;color:#bdd5e6;font-size:12px;background:#0b1721}.main{padding:28px 34px 60px;max-width:1500px;width:100%;margin:auto}.topbar{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:23px}.title h1{font-size:28px;margin:0 0 5px}.title p{margin:0;color:var(--muted);font-size:13px}.tools{display:flex;gap:8px}.tool{width:35px;height:35px;border:1px solid var(--line);border-radius:50%;display:grid;place-items:center;color:#a9bdcb;background:#0a131c}.grid{display:grid;gap:16px}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}.g3{grid-template-columns:repeat(3,minmax(0,1fr))}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.card{background:linear-gradient(155deg,rgba(17,29,40,.95),rgba(9,17,24,.98));border:1px solid var(--line);border-radius:11px;padding:18px;box-shadow:var(--shadow)}.card h3{font-size:13px;margin:0 0 14px;color:#d6e0e8}.metric-label{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#93a7b8}.metric{font-size:48px;line-height:1;font-weight:750;margin:13px 0 8px}.metric small{font-size:18px;color:#c4d0d9;font-weight:500}.orange{color:var(--orange)}.red{color:var(--red)}.green{color:var(--green)}.blue{color:var(--blue)}.muted{color:var(--muted)}.status{font-weight:700;font-size:13px}.list{display:grid;gap:11px}.row{display:grid;grid-template-columns:1fr auto;gap:18px;align-items:center;font-size:13px}.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:8px;background:var(--orange);box-shadow:0 0 9px currentColor}.badge{font-size:11px;font-weight:750}.bar{height:5px;background:#162430;border-radius:7px;overflow:hidden}.bar>span{display:block;height:100%;border-radius:7px;background:linear-gradient(90deg,var(--blue2),var(--blue));box-shadow:0 0 12px rgba(24,181,255,.7)}.bar.greenbar>span{background:linear-gradient(90deg,#178d43,var(--green))}.btn{appearance:none;border:1px solid #276fd8;border-radius:6px;background:linear-gradient(180deg,#174fae,#11377d);color:white;padding:10px 16px;font-weight:700;font-size:12px;cursor:pointer;box-shadow:0 0 18px rgba(32,104,255,.2)}.btn:hover{filter:brightness(1.12)}.btn.secondary{border-color:#2b3f51;background:#0d1924;color:#d8e5ef}.input{width:100%;border:1px solid #263a4a;background:#0b151e;color:#eef5fa;border-radius:5px;padding:9px 10px;outline:none}.input:focus{border-color:#1e8fd2;box-shadow:0 0 0 2px rgba(24,181,255,.1)}label{display:block;color:#9fb0be;font-size:11px;margin:0 0 5px}.field{margin-bottom:12px}.steps{display:grid;grid-template-columns:repeat(4,1fr);margin:7px 0 22px}.step{text-align:center;color:#71889a;font-size:11px;position:relative}.step:before{content:"";position:absolute;top:11px;left:-50%;right:50%;height:1px;background:#293b49}.step:first-child:before{display:none}.step span{position:relative;z-index:2;margin:auto auto 7px;width:24px;height:24px;border:1px solid #3a4a57;border-radius:50%;display:grid;place-items:center;background:#0c1620}.step.active{color:#a9ddff}.step.active span{border-color:#2f7bff;background:#1551b8;color:white;box-shadow:0 0 16px rgba(33,111,255,.45)}.table{width:100%;border-collapse:collapse;font-size:12px}.table th{text-align:left;color:#7f95a7;font-size:10px;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #22313d;padding:10px}.table td{padding:11px 10px;border-bottom:1px solid #172631;color:#d7e2ea}.table tr:last-child td{border-bottom:none}.recommend{display:flex;gap:9px;align-items:flex-start;font-size:12px;margin:9px 0;color:#d7e2ea}.check{color:var(--green);font-weight:900}.warning{border:1px solid #51371d;background:#19150e;border-radius:7px;padding:11px;color:#eebc72;font-size:12px}.okbox{border:1px solid #1c5830;background:#0d1b13;border-radius:7px;padding:11px;color:#95e8af;font-size:12px}.hero-score{display:flex;justify-content:center;align-items:center;min-height:190px}.ring{width:150px;height:150px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(var(--blue) var(--pct),#16232e 0);position:relative;box-shadow:0 0 36px rgba(24,181,255,.14)}.ring:after{content:"";position:absolute;width:122px;height:122px;border-radius:50%;background:#0a141d}.ring strong{position:relative;z-index:1;font-size:36px}.ring strong small{font-size:13px;color:#91a5b4}.kpi{display:flex;justify-content:space-between;gap:10px;padding:10px 0;border-bottom:1px solid #1d2b37;font-size:12px}.kpi:last-child{border:0}.tabs{display:flex;gap:24px;border-bottom:1px solid #22313e;margin-bottom:18px}.tab{padding:0 0 10px;color:#8598a8;font-size:12px}.tab.active{color:var(--blue);border-bottom:2px solid var(--blue)}.hero{padding:26px;border:1px solid #1c3a50;border-radius:12px;background:radial-gradient(circle at 75% 35%,rgba(24,181,255,.11),transparent 35%),linear-gradient(135deg,#101d29,#09121a);overflow:hidden}.hero h2{font-size:31px;margin:5px 0 8px}.hero p{max-width:760px;color:#91a7b7;line-height:1.55;font-size:13px}.eyebrow{color:var(--blue);text-transform:uppercase;letter-spacing:.12em;font-size:10px;font-weight:800}.split{display:grid;grid-template-columns:1.2fr .8fr;gap:16px}.empty{padding:40px;text-align:center;color:#718798}.footer{margin-top:24px;color:#536979;font-size:10px;text-align:center}.donut{height:8px;border-radius:10px;background:#15242f;overflow:hidden}.donut span{display:block;height:100%;background:linear-gradient(90deg,var(--orange),var(--red))}.pill{display:inline-block;border:1px solid #294053;border-radius:20px;padding:5px 9px;font-size:10px;color:#adc2d2}.footer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.mobile-nav{display:none}
@media(max-width:900px){.shell{display:block}.sidebar{display:none}.mobile-nav{display:flex;position:sticky;top:0;z-index:10;background:#09121a;border-bottom:1px solid var(--line);padding:10px;overflow:auto;gap:6px}.mobile-nav a{white-space:nowrap;border:1px solid #213545;border-radius:6px;padding:8px 10px;color:#adbfcb;font-size:11px}.main{padding:20px 14px 50px}.g2,.g3,.g4,.split{grid-template-columns:1fr}.topbar{margin-top:8px}.metric{font-size:40px}}
"""

NAV = [
    ("/", "⌂", "Dashboard"),
    ("/scan", "▣", "Scenario Scan"),
    ("/results", "◎", "Resultaten"),
    ("/stress", "◌", "Stress Test"),
    ("/reports", "▤", "Rapporten"),
    ("/governance", "♢", "Gegevens & Governance"),
]


def esc(value):
    return html.escape(str(value), quote=True)


def response(start_response, body, status="200 OK", content_type="text/html; charset=utf-8", headers=None):
    raw = body.encode("utf-8") if isinstance(body, str) else body
    h = [("Content-Type", content_type), ("Content-Length", str(len(raw)))]
    if headers:
        h.extend(headers)
    start_response(status, h)
    return [raw]


def parse_cookie(environ):
    cookie = environ.get("HTTP_COOKIE", "")
    out = {}
    for item in cookie.split(";"):
        if "=" in item:
            k, v = item.strip().split("=", 1)
            out[k] = v
    return out


def scenario_cookie(scenario):
    payload = json.dumps(scenario, separators=(",", ":")).encode()
    return base64.urlsafe_b64encode(payload).decode()


def load_scenario(environ):
    token = parse_cookie(environ).get("safeevac_scenario")
    if not token:
        return None
    try:
        return json.loads(base64.urlsafe_b64decode(token.encode()).decode())
    except Exception:
        return None


def scenario_df(s):
    return pd.DataFrame([s])[FEATURES]


@lru_cache(maxsize=1)
def get_model():
    return train_model()


@lru_cache(maxsize=1)
def get_bias_metrics():
    df = pd.read_csv(TITANIC_PATH)
    num = ["Pclass", "Age", "SibSp", "Parch", "Fare"]
    cat = ["Sex", "Embarked"]
    prep = ColumnTransformer([
        ("num", Pipeline([("imp", SimpleImputer(strategy="median")), ("scale", StandardScaler())]), num),
        ("cat", Pipeline([("imp", SimpleImputer(strategy="most_frequent")), ("oh", OneHotEncoder(handle_unknown="ignore"))]), cat),
    ])
    pipe = Pipeline([("prep", prep), ("rf", RandomForestClassifier(n_estimators=180, min_samples_leaf=2, random_state=42, n_jobs=1))])
    pipe.fit(df[num + cat], df["Survived"])
    sample = df[num + cat].dropna(subset=["Sex"]).sample(min(250, len(df)), random_state=42).copy()
    original = pipe.predict_proba(sample)[:, 1]
    flip = sample.copy(); flip["Sex"] = flip["Sex"].map({"male":"female", "female":"male"})
    sex_pp = float((abs(original - pipe.predict_proba(flip)[:, 1]) * 100).mean())
    cls = sample.copy(); cls["Pclass"] = cls["Pclass"].map({1:3,2:2,3:1})
    class_pp = float((abs(original - pipe.predict_proba(cls)[:, 1]) * 100).mean())
    return len(df), sex_pp, class_pp


def nav_html(path):
    links = []
    for href, icon, label in NAV:
        active = "active" if (path == href or (href != "/" and path.startswith(href))) else ""
        links.append(f'<a class="{active}" href="{href}"><span class="ico">{icon}</span>{esc(label)}</a>')
    return "".join(links)


def layout(path, title, subtitle, content):
    mobile = "".join(f'<a href="{h}">{esc(l)}</a>' for h, _, l in NAV)
    return f"""<!doctype html><html lang='nl'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
<title>{esc(title)} · SafeEvac AI</title><style>{CSS}</style></head><body>
<div class='mobile-nav'>{mobile}</div><div class='shell'><aside class='sidebar'><div class='brand'><span class='shield'>◇</span>SafeEvac AI</div><nav class='nav'>{nav_html(path)}</nav><div class='ai-pill'>◇ AI · human oversight</div></aside>
<main class='main'><div class='topbar'><div class='title'><h1>{esc(title)}</h1><p>{esc(subtitle)}</p></div><div class='tools'><span class='tool'>?</span><span class='tool'>◔</span><span class='tool'>●</span></div></div>{content}<div class='footer'>SafeEvac AI prototype · Decision support only · Geen automatische persoonsprioritering</div></main></div></body></html>"""


def analyze(s):
    model, metrics = get_model()
    df = scenario_df(s)
    issues = validate_scenario(df)
    if issues:
        return {"issues": issues, "metrics": metrics}
    risk, p10, p90 = predict_with_uncertainty(model, df)
    readiness = max(0.0, min(100.0, 100.0 - risk))
    actions = recommended_interventions(model, df, risk)
    return {"risk": risk, "readiness": readiness, "p10": p10, "p90": p90, "band": risk_band(risk), "actions": actions, "metrics": metrics}


def dashboard(environ):
    s = load_scenario(environ) or DEFAULT_SCENARIO
    a = analyze(s)
    readiness = a.get("readiness", 68)
    risk = a.get("risk", 62)
    actions = a.get("actions", [])[:5]
    risks = []
    if s["blocked_exits"]: risks.append(("Beperkte uitgangscapaciteit", "Hoog", "red"))
    support = s["mobility_support_count"] + s["sensory_cognitive_support_count"]
    if support > s["bhv_count"]: risks.append(("Assistentiebehoefte vs. capaciteit", "Middel", "orange"))
    if s["alarm_delay_s"] > 60: risks.append(("Lange alarm-/responstijd", "Middel", "orange"))
    if not s["drill_recent"]: risks.append(("Geen recente oefening", "Laag", "orange"))
    if s["communication_channels"] < 3: risks.append(("Communicatiekanalen beperkt", "Laag", "orange"))
    risks = (risks + [("Bezoekers kennen de locatie beperkt", "Laag", "orange")])[:5]
    risk_rows = "".join(f'<div class="row"><span><i class="dot"></i>{esc(x)}</span><b class="badge {c}">{esc(b)}</b></div>' for x,b,c in risks)
    improvement = sum(max(0, x["risk_reduction"]) for x in actions[:3]) if actions else 0
    content = f"""
<div class='hero'><div class='eyebrow'>Responsible AI · Emergency Preparedness</div><h2>Zie kwetsbaarheden vóór een calamiteit.</h2><p>SafeEvac stress-test evacuatieplannen met gebouw-, bezettings- en responsdata. De AI adviseert waar je plan versterkt kan worden; een veiligheidsprofessional blijft eindverantwoordelijk.</p><div class='footer-actions'><a class='btn' href='/scan'>Nieuwe scenario scan</a><a class='btn secondary' href='/stress'>Stress test</a></div></div>
<div class='grid g2' style='margin-top:16px'>
 <div class='card'><div class='grid g2'><div><div class='metric-label'>Evacuation Readiness Score</div><div class='metric orange'>{readiness:.0f}<small>/100</small></div><div class='status orange'>{'Goed' if readiness>=70 else 'Matig' if readiness>=45 else 'Laag'}</div><p class='muted' style='font-size:11px'>Laatste opgeslagen browser-scenario</p><a class='btn' href='/scan'>Nieuwe scan</a></div><div class='hero-score'><div class='ring' style='--pct:{readiness:.0f}%'><strong>{readiness:.0f}<small>/100</small></strong></div></div></div></div>
 <div class='card'><h3>Top risico's</h3><div class='list'>{risk_rows}</div></div>
 <div class='card'><h3>Verbeterpotentieel</h3><div class='metric blue'>+{improvement:.0f}<small> pt</small></div><p class='muted' style='font-size:12px'>indicatieve verbetering uit de sterkste model-hefbomen</p><div class='bar greenbar'><span style='width:{min(100, improvement*3):.0f}%'></span></div></div>
 <div class='card'><h3>Operationeel risico</h3><div class='metric red'>{risk:.0f}<small>/100</small></div><div class='donut'><span style='width:{risk:.0f}%'></span></div><p class='muted' style='font-size:11px'>Geen certificeringsscore; bedoeld voor scenario- en managementondersteuning.</p></div>
</div>"""
    return layout("/", "Dashboard", "Overzicht van evacuatiegereedheid", content)


def option_list(values, selected):
    return "".join(f'<option value="{esc(v)}" {"selected" if v==selected else ""}>{esc(v)}</option>' for v in values)


def number_field(name, label, value, step="1", minv="0", maxv="10000"):
    return f'<div class="field"><label>{esc(label)}</label><input class="input" type="number" name="{name}" value="{esc(value)}" step="{step}" min="{minv}" max="{maxv}" required></div>'


def scan_page(environ, method, post):
    s = load_scenario(environ) or DEFAULT_SCENARIO.copy()
    if method == "POST":
        try:
            s = {
                "sector": post.get("sector", ["Office"])[0], "hazard": post.get("hazard", ["Fire"])[0],
                "occupant_count": int(post["occupant_count"][0]), "floors": int(post["floors"][0]),
                "exits_total": int(post["exits_total"][0]), "blocked_exits": int(post["blocked_exits"][0]),
                "stair_width_m": float(post["stair_width_m"][0]), "avg_route_m": float(post["avg_route_m"][0]),
                "bhv_count": int(post["bhv_count"][0]), "mobility_support_count": int(post["mobility_support_count"][0]),
                "sensory_cognitive_support_count": int(post["sensory_cognitive_support_count"][0]),
                "visitor_pct": float(post["visitor_pct"][0]), "alarm_delay_s": int(post["alarm_delay_s"][0]),
                "trained_occupants_pct": float(post["trained_occupants_pct"][0]),
                "communication_channels": int(post["communication_channels"][0]),
                "night_shift": 1 if "night_shift" in post else 0, "drill_recent": 1 if "drill_recent" in post else 0,
            }
            issues = validate_scenario(scenario_df(s))
            if not issues:
                token = scenario_cookie(s)
                return None, [("Location", "/results"), ("Set-Cookie", f"safeevac_scenario={token}; Path=/; SameSite=Lax")]
            error = " ".join(issues)
        except Exception as exc:
            error = f"Controleer de ingevoerde waarden: {exc}"
    else:
        error = ""
    content = f"""
<div class='steps'><div class='step active'><span>1</span>Basisinfo</div><div class='step'><span>2</span>Bezetting & Personen</div><div class='step'><span>3</span>Hulpbronnen</div><div class='step'><span>4</span>Incident & Omgeving</div></div>
{f'<div class="warning" style="margin-bottom:14px">{esc(error)}</div>' if error else ''}
<form method='post' class='card'><div class='split'><div><h3>Basisinformatie</h3>
<div class='field'><label>Sector</label><select class='input' name='sector'>{option_list(SECTORS,s['sector'])}</select></div>
<div class='field'><label>Scenario</label><select class='input' name='hazard'>{option_list(HAZARDS,s['hazard'])}</select></div>
{number_field('floors','Aantal verdiepingen',s['floors'],minv='1',maxv='50')}
{number_field('occupant_count','Aantal aanwezige personen',s['occupant_count'],minv='1')}
{number_field('exits_total','Aantal uitgangen',s['exits_total'],minv='1',maxv='30')}
{number_field('blocked_exits','Geblokkeerde uitgangen',s['blocked_exits'],minv='0',maxv='29')}
{number_field('stair_width_m','Gemiddelde trap-/egressbreedte (m)',s['stair_width_m'],step='0.1',minv='0.5',maxv='5')}
{number_field('avg_route_m','Gemiddelde evacuatieroute (m)',s['avg_route_m'],step='5',minv='5',maxv='500')}
</div><div><h3>Mensen, ondersteuning & respons</h3>
{number_field('bhv_count','Beschikbare BHV/responders',s['bhv_count'],minv='1',maxv='500')}
{number_field('mobility_support_count','Mobiliteitsondersteuning nodig',s['mobility_support_count'],minv='0')}
{number_field('sensory_cognitive_support_count','Extra communicatie/begeleiding nodig',s['sensory_cognitive_support_count'],minv='0')}
{number_field('visitor_pct','Bezoekers / onbekend met locatie (%)',s['visitor_pct'],step='1',minv='0',maxv='100')}
{number_field('trained_occupants_pct','Bekend met evacuatieprocedure (%)',s['trained_occupants_pct'],step='1',minv='0',maxv='100')}
{number_field('alarm_delay_s','Alarm + responsvertraging (sec)',s['alarm_delay_s'],step='5',minv='0',maxv='600')}
{number_field('communication_channels','Redundante communicatiekanalen',s['communication_channels'],minv='1',maxv='5')}
<div class='field'><label><input type='checkbox' name='night_shift' {'checked' if s['night_shift'] else ''}> Nacht-/lage-bezettingsorganisatie</label></div>
<div class='field'><label><input type='checkbox' name='drill_recent' {'checked' if s['drill_recent'] else ''}> Recent geoefend en geëvalueerd</label></div>
</div></div><div class='footer-actions'><button class='btn' type='submit'>Analyseer scenario →</button></div></form>"""
    return layout("/scan", "Nieuwe scenario scan", "Voer de kernwaarden van gebouw en situatie in", content), None


def results_page(environ):
    s = load_scenario(environ)
    if not s:
        return layout("/results", "Scan resultaten", "Nog geen scenario beschikbaar", "<div class='card empty'>Start eerst een <a class='blue' href='/scan'>Scenario Scan</a>.</div>")
    a = analyze(s)
    if a.get("issues"):
        return layout("/results", "Scan resultaten", "Validatieprobleem", f"<div class='warning'>{esc(' '.join(a['issues']))}</div>")
    risk, readiness = a["risk"], a["readiness"]
    action_rows = "".join(f'<div class="kpi"><span>{i}. {esc(x["action"])}</span><b class="green">+{x["risk_reduction"]:.1f}</b></div>' for i,x in enumerate(a["actions"][:5],1)) or '<p class="muted">Geen directe model-hefbomen gevonden.</p>'
    recs = "".join(f'<div class="recommend"><span class="check">●</span><span>{esc(x["action"])}</span></div>' for x in a["actions"][:3])
    total_support=s['mobility_support_count']+s['sensory_cognitive_support_count']
    content=f"""
<div class='grid g3'><div class='card'><div class='metric-label'>Evacuation Readiness Score</div><div class='metric orange'>{readiness:.0f}<small>/100</small></div><div class='status orange'>{'Goed' if readiness>=70 else 'Matig' if readiness>=45 else 'Laag'}</div></div>
<div class='card'><div class='metric-label'>Operationeel risico</div><div class='metric red'>{risk:.0f}<small>/100</small></div><div class='status red'>{esc(a['band'])}</div></div>
<div class='card'><div class='metric-label'>Onzekerheid (modelspreiding)</div><div class='metric blue' style='font-size:34px'>{a['p10']:.0f}–{a['p90']:.0f}</div><div class='status green'>P10–P90 bomen</div></div></div>
<div class='grid g2' style='margin-top:16px'><div class='card'><h3>Belangrijkste context</h3><div class='kpi'><span>Sector / scenario</span><b>{esc(s['sector'])} · {esc(s['hazard'])}</b></div><div class='kpi'><span>Bezetting</span><b>{s['occupant_count']}</b></div><div class='kpi'><span>Uitgangen</span><b>{s['exits_total']-s['blocked_exits']} bruikbaar / {s['exits_total']} totaal</b></div><div class='kpi'><span>Extra ondersteuningsvragen</span><b>{total_support}</b></div><div class='kpi'><span>BHV/responders</span><b>{s['bhv_count']}</b></div></div>
<div class='card'><h3>Grootste verbeteringen (impact)</h3>{action_rows}</div></div>
<div class='card' style='margin-top:16px'><h3>Aanbevolen acties</h3>{recs}<div class='okbox' style='margin-top:14px'>Human-in-the-loop: SafeEvac adviseert over capaciteit en kwetsbaarheden; de AI bepaalt nooit wie hulp krijgt of wie eerst geëvacueerd wordt.</div><div class='footer-actions'><a class='btn secondary' href='/stress'>Stress test</a><a class='btn' href='/download-report'>Rapport downloaden</a></div></div>"""
    return layout("/results", "Scan resultaten", f"{s['sector']} · {s['hazard']}", content)


def stress_page(environ):
    s=load_scenario(environ)
    if not s:
        return layout("/stress","Stress test","Bekijk wat er verandert bij verstoringen","<div class='card empty'>Start eerst een <a class='blue' href='/scan'>Scenario Scan</a>.</div>")
    model,_=get_model(); df=scenario_df(s); base=float(model.predict(df[FEATURES])[0])
    cases=[
        ("Extra uitgang geblokkeerd","blocked_exits",min(s['exits_total']-1,s['blocked_exits']+1)),
        ("Alarmvertraging +90 sec","alarm_delay_s",min(600,s['alarm_delay_s']+90)),
        ("Communicatie uitval","communication_channels",1),
        ("Nachtbezetting","night_shift",1),
        ("Geen recente oefening","drill_recent",0),
    ]
    results=[]
    for label,feature,value in cases:
        alt=df.copy(); alt.loc[0,feature]=value; val=float(model.predict(alt[FEATURES])[0]); results.append((label,val,val-base))
    worst=max(results,key=lambda x:x[2])
    rows="".join(f'<tr><td>{esc(l)}</td><td class="{ "red" if d>5 else "orange" if d>0 else "green"}">{v:.1f}</td><td>{d:+.1f}</td></tr>' for l,v,d in sorted(results,key=lambda x:x[2],reverse=True))
    pts=[base]+[x[1] for x in results]; maxv=max(100,max(pts)); coords=[]
    for i,v in enumerate(pts):
        x=20+i*(360/(len(pts)-1)); y=150-(v/maxv)*120; coords.append(f"{x:.0f},{y:.0f}")
    content=f"""<div class='split'><div class='card'><h3>Stressfactoren</h3><table class='table'><thead><tr><th>Scenario</th><th>Risico</th><th>Δ</th></tr></thead><tbody>{rows}</tbody></table></div><div class='grid'><div class='card'><h3>Resultaat zwaarste test</h3><div class='grid g2'><div><div class='metric-label'>Nieuwe score</div><div class='metric red'>{worst[1]:.0f}<small>/100</small></div></div><div><div class='metric-label'>Verandering</div><div class='metric red'>{worst[2]:+.0f}<small> pt</small></div></div></div></div>
<div class='card'><h3>Score verloop</h3><svg viewBox='0 0 400 170' width='100%' height='170'><defs><linearGradient id='g' x1='0' x2='1'><stop stop-color='#195cff'/><stop offset='1' stop-color='#18b5ff'/></linearGradient></defs><path d='M20 150 H380' stroke='#233745'/><polyline fill='none' stroke='url(#g)' stroke-width='3' points='{" ".join(coords)}'/>{''.join(f'<circle cx="{p.split(",")[0]}" cy="{p.split(",")[1]}" r="4" fill="#18b5ff"/>' for p in coords)}</svg></div></div></div><div class='warning' style='margin-top:16px'>Impact: <b>{esc(worst[0])}</b> heeft in dit model de grootste negatieve invloed. Gebruik dit om een oefening of managementbesluit voor te bereiden, niet als realtime automatische evacuatiebesturing.</div>"""
    return layout("/stress","Stress test","Bekijk hoe je score verandert bij verschillende situaties",content)


def reports_page(environ):
    s=load_scenario(environ)
    if s:
        a=analyze(s); score=f"{a.get('readiness',0):.0f}"; risk=f"{a.get('risk',0):.0f}"
        current=f"<tr><td>Laatste SafeEvac scan</td><td>{esc(s['sector'])} · {esc(s['hazard'])}</td><td>Browser-sessie</td><td class='orange'>{score}</td><td><a class='blue' href='/download-report'>Download</a></td></tr>"
    else:
        current="<tr><td colspan='5' class='muted'>Nog geen scenario in deze browser.</td></tr>"
    content=f"""<div class='card'><div style='display:flex;justify-content:space-between;align-items:center'><h3>Gegenereerde rapporten</h3><a class='btn' href='/scan'>Nieuw rapport</a></div><table class='table'><thead><tr><th>Rapport naam</th><th>Scenario</th><th>Datum</th><th>Score</th><th></th></tr></thead><tbody>{current}</tbody></table></div><div class='card' style='margin-top:16px'><h3>Rapport bevat</h3><div class='grid g4'><div class='card'><b class='blue'>▤</b><p>Scenario overzicht</p></div><div class='card'><b class='blue'>◎</b><p>Risico analyse</p></div><div class='card'><b class='blue'>◇</b><p>Aanbevelingen</p></div><div class='card'><b class='blue'>◌</b><p>Stress test context</p></div></div></div>"""
    return layout("/reports","Rapporten","Overzicht en export",content)


def governance_page(environ):
    _,metrics=get_model(); n,sex_pp,class_pp=get_bias_metrics()
    used=[("Gebouwkenmerken","verdiepingen, uitgangen, route, egressbreedte"),("Bezetting","aantallen en bezoekerspercentage"),("Hulpbronnen","BHV, communicatie, alarmrespons"),("Inclusieve planning","geaggregeerde ondersteuningsbehoefte"),("Context","sector, hazard, nachtorganisatie")]
    excluded=["Geslacht","Leeftijd per persoon","Gezondheidsdiagnose","Nationaliteit","Inkomen / sociale klasse","Andere persoonskenmerken"]
    used_html="".join(f'<div class="kpi"><span>▧ {esc(a)}</span><b class="muted">{esc(b)}</b></div>' for a,b in used)
    ex_html="".join(f'<div class="kpi"><span class="red">×</span><span style="flex:1">{esc(x)}</span></div>' for x in excluded)
    content=f"""<div class='tabs'><span class='tab active'>Data gebruik</span><span class='tab'>Model informatie</span><span class='tab'>Ethiek & Bias</span><span class='tab'>Beperkingen</span></div><div class='grid g2'><div class='card'><h3>Gebruikte data (geen individuele persoonsprofielen)</h3>{used_html}<div class='kpi'><span>Prototype trainingsdata</span><b>6.000 scenario's</b></div></div><div class='card'><h3>Bewust uitgesloten</h3>{ex_html}</div></div>
<div class='grid g3' style='margin-top:16px'><div class='card'><div class='metric-label'>Model MAE</div><div class='metric blue'>{metrics.mae:.1f}</div><p class='muted'>risicopunten op synthetische hold-out data</p></div><div class='card'><div class='metric-label'>R²</div><div class='metric blue'>{metrics.r2:.2f}</div><p class='muted'>prototype fit, geen real-world certificering</p></div><div class='card'><div class='metric-label'>Titanic bias demo</div><div class='metric red' style='font-size:31px'>{sex_pp:.1f}<small> pp</small></div><p class='muted'>gemiddelde gevoeligheid bij sex-flip; klasse-flip {class_pp:.1f} pp over {n} historische rijen</p></div></div>
<div class='card' style='margin-top:16px'><div style='display:flex;gap:14px;align-items:center'><div class='shield' style='color:var(--green);border-color:#27793f'>✓</div><div><h3 style='margin:0 0 5px'>Ons commitment</h3><p class='muted' style='margin:0;font-size:12px'>SafeEvac ondersteunt besluitvorming. Het systeem maakt nooit onderscheid tussen personen en bepaalt niet wie hulp krijgt. Voor productie zijn echte ontruimingsoefeningen, gebouwdata en onafhankelijke safety-validatie nodig.</p></div></div></div>"""
    return layout("/governance","Gegevens & Governance","Transparantie over data, model en ethiek",content)


def report_download(environ, start_response):
    s=load_scenario(environ)
    if not s:
        return response(start_response,"Geen scenario beschikbaar.","404 Not Found","text/plain; charset=utf-8")
    a=analyze(s); out=io.StringIO(); w=csv.writer(out)
    w.writerow(["SafeEvac AI Evacuation Readiness Report"]); w.writerow([])
    w.writerow(["Readiness score",f"{a['readiness']:.1f}/100"]); w.writerow(["Operational risk",f"{a['risk']:.1f}/100"]); w.writerow(["Risk band",a['band']]); w.writerow(["Model spread P10-P90",f"{a['p10']:.1f}-{a['p90']:.1f}"]); w.writerow([])
    w.writerow(["Scenario field","Value"])
    for k,v in s.items(): w.writerow([k,v])
    w.writerow([]); w.writerow(["Recommended intervention","Estimated risk reduction"])
    for x in a['actions'][:5]: w.writerow([x['action'],f"{x['risk_reduction']:.1f}"])
    w.writerow([]); w.writerow(["Governance note","Decision support only; no automatic person ranking or evacuation priority."])
    data=out.getvalue().encode('utf-8-sig')
    return response(start_response,data,content_type='text/csv; charset=utf-8',headers=[('Content-Disposition','attachment; filename="safeevac_report.csv"')])


def application(environ, start_response):
    path=environ.get('PATH_INFO','/') or '/'; method=environ.get('REQUEST_METHOD','GET').upper()
    try:
        if path=='/': return response(start_response,dashboard(environ))
        if path=='/scan':
            post={}
            if method=='POST':
                try: length=int(environ.get('CONTENT_LENGTH') or 0)
                except ValueError: length=0
                body=environ['wsgi.input'].read(length).decode('utf-8','replace'); post=parse_qs(body,keep_blank_values=True)
            page, headers=scan_page(environ,method,post)
            if headers:
                return response(start_response,b'',status='303 See Other',content_type='text/plain; charset=utf-8',headers=headers)
            return response(start_response,page)
        if path=='/results': return response(start_response,results_page(environ))
        if path=='/stress': return response(start_response,stress_page(environ))
        if path=='/reports': return response(start_response,reports_page(environ))
        if path=='/governance': return response(start_response,governance_page(environ))
        if path=='/download-report': return report_download(environ,start_response)
        return response(start_response,layout(path,'Niet gevonden','404',"<div class='card empty'>Deze pagina bestaat niet. <a class='blue' href='/'>Terug naar dashboard</a>.</div>"),'404 Not Found')
    except Exception as exc:
        body=layout(path,'Applicatiefout','SafeEvac kon deze aanvraag niet verwerken',f"<div class='warning'><b>Fout:</b> {esc(exc)}<br><br>Controleer of de Python packages uit requirements.txt zijn geïnstalleerd en de data-map aanwezig is.</div>")
        return response(start_response,body,'500 Internal Server Error')
