#!/usr/bin/env python3
"""
build_hospital_graph.py

Builds a Neo4j graph linking hospitals in Podlaskie voivodeship,
their directors, supervisory-board members, political parties,
and regional councilors.

Usage:
    python3 build_hospital_graph.py

Neo4j connection (env vars):
    NEO4J_URI  (default: bolt://localhost:7687)
    NEO4J_USER (default: neo4j)
    NEO4J_PASS (default: graphdb123)
"""

import json
import os
import re
import textwrap
import unicodedata
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import Optional

from neo4j import GraphDatabase

# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class Party:
    slug: str      # normalized key, e.g. "pis", "koalicja-obywatelska"
    name: str      # display name, e.g. "Prawo i Sprawiedliwość"


@dataclass
class Person:
    uid: str
    name: str
    party_note: str = ""    # context string (e.g. "brak/niepotwierdzona", or compound)
    party_slugs: list = field(default_factory=list)  # list of Party slugs
    source_urls: list = field(default_factory=list)
    note: str = ""

    @property
    def norm_name(self):
        return norm_name(self.name)


@dataclass
class Hospital:
    id: str
    name: str
    city: str
    legal_form: str = ""
    website: str = ""
    notes: str = ""


@dataclass
class Role:
    hospital_id: str
    person: Person
    role: str  # "DYREKTOR", "Z_CA", "RADA_SPOLECZNA", "RADNIA", "PREZES_ZARZADU", "RADA_NADZORCZA"
    sub_role: str = ""  # e.g. "Przewodniczacy", "Z-ca ds. Lecznictwa"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def norm_name(s: str) -> str:
    """Normalise a person name for deduplication."""
    s = unicodedata.normalize("NFKD", s)
    s = re.sub(r"[^\w\s\-\.]+", "", s, flags=re.UNICODE)
    s = re.sub(r"\s+", " ", s).strip().lower()
    # Remove academic titles
    s = re.sub(
        r"\b(prof\.\s*dr\s*hab\s*dr\s*inż?\s*inż\.\s*mgr\s*dr\s*inż?\s*)"
        r"|(\binż?\.\s*dr\s*hab\s*hab\.\s*dr\s*hab)\b",
        "", s, flags=re.IGNORECASE
    )
    return s


# ---------------------------------------------------------------------------
# Party normalisation
# ---------------------------------------------------------------------------

_PARTY_CANONICAL = {
    # Major parties
    "pis": ("Prawo i Sprawiedliwość", "pis"),
    "pi-sprawiedliwosc": ("Prawo i Sprawiedliwość", "pis"),
    "prawo-i-sprawiedliwosc": ("Prawo i Sprawiedliwość", "pis"),
    "koalicja-obywatelska": ("Koalicja Obywatelska", "ko"),
    "ko": ("Koalicja Obywatelska", "ko"),
    "po": ("Platforma Obywatelska", "ko"),
    "ko-po": ("Koalicja Obywatelska", "ko"),
    "po-ko": ("Koalicja Obywatelska", "ko"),
    "psl": ("Polskie Stronnictwo Ludowe", "psl"),
    "polskie-ustronnictwo-ludowe": ("Polskie Stronnictwo Ludowe", "psl"),
    "polska-2050": ("Polska 2050", "polska-2050"),
    "trzecia-droga": ("Trzecia Droga", "trzecia-droga"),
    "slp": ("SLP", "slp"),
    "sld": ("Sojusz Lewicy Demokratycznej", "sld"),
    "lewica": ("Lewica", "lewica"),
    "prawica-rzeczypospolitej": ("Prawica Rzeczypospolitej", "prawica-rzeczpospolita"),
    "prawa-rzeczypospolitej": ("Prawica Rzeczypospolitej", "prawica-rzeczpospolita"),

    # KWW / local committees - keep them as-is but normalized
    "kww-ziemia-hajnowska": ("KWW Ziemia Hajnowska", "kww-ziemia-hajnowska"),
    "kww-razem-dla-sokolki": ("KWW Razem dla Sokółki", "kww-razem-dla-sokolki"),
    "kww-nasze-podlasie": ("KWW Nasze Podlasie", "kww-nasze-podlasie"),
    "kww-przyjazna-lomza": ("KWW Przyjazna Łomża", "kww-przyjazna-lomza"),
    "kww-samorzad-powiatu-augustowskiego": ("KWW Samorząd Powiatu Augustowskiego", "kww-spa"),
    "kww-spa": ("KWW Samorząd Powiatu Augustowskiego", "kww-spa"),
    "kww-bau-2024": ("KWW BAU 2024", "kww-bau-2024"),
    "kww-suwalskie-porozumienie-prawicy": ("KWW Suwalskie Porozumienie Prawicy", "kww-sup"),
    "kww-hajnowa-razem": ("KWW Hajnówka Razem", "kww-hajnowa-razem"),
    "kww-nasza-przyszlosc": ("KWW Nasza Przyszłość", "kww-nasza-przyszlosc"),
    "kww-roztrotna-troska-o-dobro-gminy": ("KWW Roztrotna Troska o Dobro Gminy", "kww-rt"),
    "kww-narew-odnowa": ("KWW Narew Odnowa", "kww-narew-odnowa"),
    "kww-wspolna-narewka": ("KWW Wspólna Narewka", "kww-wspolna-narewka"),
    "kww-spw": ("KWW SPW", "kww-spw"),
    "kww-wlasny": ("KWW Własny", "kww-wlasny"),
    "kww-bmn-aktywny-samorzad": ("KWW BMN Aktywny Samorząd", "kww-bmn"),
    "kww-dla-siemiatycz-razem": ("KWW Dla Siemiatycz Razem", "kww-dla-siemiatycz-razem"),
    "kww-macieja-pawla-bednarko": ("KWW Macieja Pawła Bednarko", "kww-mpb"),
    "kww-dobry-powiat": ("KWW Dobry Powiat", "kww-dobry-powiat"),
    "kww-skuteczny-i-przyjazny-samorzad": ("KWW Skuteczny i Przyjazny Samorząd", "kww-sps"),
    "kww-ziemia-sejneńska": ("KWW Ziemia Sejneńska", "kww-sejny"),
    "kww-razem-dla-suwalk": ("KWW Razem dla Suwałk", "kww-razem-dla-suwalk"),
    "kww-porozumienie-samorzadowe-powiatu-kolno": ("KWW Porozumienie Samorządowe Powiatu Kolno", "kww-pspk"),
    "kww-pspk": ("KWW Porozumienie Samorządowe Powiatu Kolno", "kww-pspk"),
    "kw-koalicja-bielska": ("KW Koalicja Bielska", "kw-koalicja-bielska"),
}

# Second-level mapping for names that didn't match the first pass
_PARTY_SECONDARY = {
    "historycznie sld/lewica": ("historycznie SLD/Lewica", "sld"),
    "możliwie trzecia droga": ("Trzecia Droga", "trzecia-droga"),
    "wicemarszałek woj.; koalicja obywatelska; niezrzeszony": ("Koalicja Obywatelska", "ko"),
}


def normalise_party(raw: str) -> tuple[str, str]:
    """
    Given a raw party string, return (canonical_name, slug).
    If no match, normalise the input itself.
    """
    key = re.sub(r"[^\w\s\-\/\.]+", "", raw).strip().lower()
    # Replace Polish characters
    key = unicodedata.normalize("NFD", key)
    key = re.sub(r"[^\w\s\-]+", "", key)

    # Check if it's a KWW/KW local committee
    if "kww" in key or "kw " in key:
        display = re.sub(r"\s+", " ", raw.strip().title())
        slug = re.sub(r"[^\w\-]+", "-", display.lower())
        slug = re.sub(r"-+", "-", slug).strip("-")
        return display, slug

    if key in _PARTY_CANONICAL:
        return _PARTY_CANONICAL[key]

    # Check secondary (full-string) matches
    if key in _PARTY_SECONDARY:
        return _PARTY_SECONDARY[key]

    # Check for compound strings like "Trzecia Droga (PSL / Polska 2050)" - extract main parties
    # Split on " / " or " - " or "(" and process each
    parts = re.split(r"[\(\)\/\-\,\;]+", raw)
    parts = [p.strip() for p in parts if p.strip()]

    slugs_found = []
    names_found = []
    for part in parts:
        part_key = unicodedata.normalize("NFD", re.sub(r"[^\w\s\-]+", "", part).strip().lower())
        
        # Check primary canonical matching
        if part_key in _PARTY_CANONICAL:
            name, slug = _PARTY_CANONICAL[part_key]
            if slug not in slugs_found:
                slugs_found.append(slug)
                names_found.append(name)
            continue
        
        # Check secondary canonical matching
        if part_key in _PARTY_SECONDARY:
            name, slug = _PARTY_SECONDARY[part_key]
            if slug not in slugs_found:
                slugs_found.append(slug)
                names_found.append(name)
            continue

    if slugs_found:
        return " + ".join(names_found), "|".join(slugs_found)

    # No match - use the raw string as-is (normalised)
    display = re.sub(r"\s+", " ", raw.strip().title())
    slug = re.sub(r"[^\w\-]+", "-", display.lower())
    slug = re.sub(r"-+", "-", slug).strip("-")
    return display, slug


# ---------------------------------------------------------------------------
# Neo4j helpers
# ---------------------------------------------------------------------------

BATCH_SIZE = 500


def _connect():
    uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
    user = os.getenv("NEO4J_USER", "neo4j")
    pwd = os.getenv("NEO4J_PASS", "graphdb123")
    driver = GraphDatabase.driver(uri, auth=(user, pwd))
    # Verify connectivity
    driver.verify_connectivity()
    return driver


def load_hospitals(driver, hospitals: list[Hospital]):
    """Bulk merge Hospital nodes."""
    with driver.session() as session:
        batches = [hospitals[i:i + BATCH_SIZE] for i in range(0, len(hospitals), BATCH_SIZE)]
        for batch in batches:
            records = [
                {
                    "id": h.id,
                    "name": h.name,
                    "city": h.city,
                    "legal_form": h.legal_form,
                    "website": h.website,
                    "notes": h.notes,
                }
                for h in batch
            ]
            session.execute_write(_merge_hospitals, records)


def _merge_hospitals(tx, records):
    cypher = """
    UNWIND $records AS r
    MERGE (h:Hospital {uid: r.id})
    ON CREATE SET h.name = r.name, h.city = r.city,
                  h.legalForm = r.legal_form, h.website = r.website, h.notes = r.notes
    ON MATCH SET h.name = r.name, h.city = r.city,
                 h.legalForm = r.legal_form, h.website = r.website, h.notes = r.notes
    """
    tx.run(cypher, records=records)


def load_persons(driver, persons: list[Person]):
    """Bulk merge Person nodes."""
    with driver.session() as session:
        batches = [persons[i:i + BATCH_SIZE] for i in range(0, len(persons), BATCH_SIZE)]
        for batch in batches:
            records = [
                {
                    "uid": p.uid,
                    "name": p.name,
                    "partyNote": p.party_note,
                    "partySlugs": p.party_slugs,
                    "sourceUrls": p.source_urls,
                    "note": p.note,
                    "normName": p.norm_name,
                }
                for p in batch
            ]
            session.execute_write(_merge_persons, records)


def _merge_persons(tx, records):
    cypher = """
    UNWIND $records AS r
    MERGE (p:Person {uid: r.uid})
    ON CREATE SET p.name = r.name, p.partyNote = r.partyNote,
                  p.sourceUrls = r.sourceUrls, p.note = r.note, p.normName = r.normName
    ON MATCH SET p.name = r.name, p.partyNote = r.partyNote,
                 p.note = r.note, p.normName = r.normName,
                 p.sourceUrls = p.sourceUrls + r.sourceUrls
    WITH p, r
    // Link to Party nodes via slug
    UNWIND r.partySlugs AS slug
    MATCH (pt:Party {slug: slug})
    MERGE (p)-[rel:AFILIACJA_PARTYJNA]->(pt)
    ON CREATE SET rel.createdAt = timestamp()
    """
    tx.run(cypher, records=records)


def load_parties(driver, parties: list[Party]):
    """Bulk merge Party nodes."""
    with driver.session() as session:
        records = [
            {
                "slug": p.slug,
                "name": p.name,
            }
            for p in parties
        ]
        session.execute_write(_merge_parties, records)


def _merge_parties(tx, records):
    cypher = """
    UNWIND $records AS r
    MERGE (pt:Party {slug: r.slug})
    ON CREATE SET pt.name = r.name
    ON MATCH SET pt.name = r.name
    """
    tx.run(cypher, records=records)


def load_roles(driver, roles: list[Role], persons_by_uid: dict):
    """Create relationships between hospitals and persons."""
    with driver.session() as session:
        batches = [roles[i:i + BATCH_SIZE] for i in range(0, len(roles), BATCH_SIZE)]
        for batch in batches:
            records = [
                {
                    "hospitalId": r.hospital_id,
                    "personUid": r.person.uid,
                    "relationship": r.role,
                    "subRole": r.sub_role,
                }
                for r in batch
            ]
            session.execute_write(_merge_roles, records)


def _merge_roles(tx, records):
    type_to_cypher = {
        "DYREKTOR": "MERGE (h)-[rel:DYREKTOR]->(p)",
        "Z_CA": "MERGE (h)-[rel:Z_CA]->(p)",
        "RADA_SPOLECZNA": "MERGE (h)-[rel:RADA_SPOLECZNA]->(p)",
        "RADNIA": "MERGE (h)-[rel:RADNIA]->(p)",
        "PREZES_ZARZADU": "MERGE (h)-[rel:PREZES_ZARZADU]->(p)",
        "RADA_NADZORCZA": "MERGE (h)-[rel:RADA_NADZORCZA]->(p)",
    }
    for rel_type, pattern in type_to_cypher.items():
        rel_records = [r for r in records if r["relationship"] == rel_type]
        if rel_records:
            tx.run(
                f"""
                UNWIND $records AS r
                MATCH (h:Hospital {{uid: r.hospitalId}})
                MATCH (p:Person {{uid: r.personUid}})
                {pattern}
                ON CREATE SET rel.createdAt = timestamp()
                """,
                records=rel_records
            )


def validate(driver):
    """Run validation queries and print results."""
    with driver.session() as session:
        queries = {
            "Total hospitals": "MATCH (h:Hospital) RETURN count(h) AS total",
            "Total persons": "MATCH (p:Person) RETURN count(p) AS total",
            "Total parties": "MATCH (pt:Party) RETURN count(pt) AS total",
            "Persons linked to parties": (
                "MATCH (p:Person)-[r:AFILIACJA_PARTYJNA]->(:Party) RETURN count(p) AS total"
            ),
            "Person->Party relationships": "MATCH ()-[r:AFILIACJA_PARTYJNA]->() RETURN count(r) AS total",
            "Director relationships": "MATCH ()-[rel:DYREKTOR]->() RETURN count(rel) AS total",
            "Deputy director relationships": "MATCH ()-[rel:Z_CA]->() RETURN count(rel) AS total",
            "Social board relationships": "MATCH ()-[rel:RADA_SPOLECZNA]->() RETURN count(rel) AS total",
            "Supervisory board relationships": "MATCH ()-[rel:RADA_NADZORCZA]->() RETURN count(rel) AS total",
            "Board member relationships": "MATCH ()-[rel:RADNIA]->() RETURN count(rel) AS total",
        }
        print("\n=== VALIDATION ===")
        for label, query in queries.items():
            result = session.run(query)
            record = result.single()
            val = record.get('total', record.get('overlappingPersons', 0))
            print(f"  {label}: {val}")
        print("==================\n")


def export_overlap_csv(driver, filename="overlap_report.csv"):
    """Export persons who hold multiple roles to CSV."""
    cypher = """
    MATCH (p:Person)
    OPTIONAL MATCH (p)-[r:AFILIACJA_PARTYJNA]->(pt:Party)
    RETURN p.name AS name, pt.name AS party, p.sourceUrls AS sourceUrls, p.note AS note
    ORDER BY p.name
    """
    with driver.session() as session:
        result = session.run(cypher)
        rows = [{"name": r["name"], "party": r["party"] or "", "sourceUrls": r["sourceUrls"], "note": r["note"]} for r in result]

    with open(filename, "w", encoding="utf-8") as f:
        f.write("name,party,sourceUrls,note\n")
        for row in rows:
            urls = "|".join(row["sourceUrls"]) if row["sourceUrls"] else ""
            note = row["note"].replace('"', '""') if row["note"] else ""
            f.write(f'"{row["name"]}","{row["party"]}","{urls}","{note}"\n')

    print(f"Exported {len(rows)} persons to {filename}")
    return rows


# ---------------------------------------------------------------------------
# Build the graph from embedded research data
# ---------------------------------------------------------------------------

HOSPITALS = []  # (id, name, city, legalForm, website, notes)

# === CZESC I — Szpitale wojewodzkie, uniwersyteckie i resortowe ===

HOSPITALS.append((
    "uskwb",
    "Uniwersytecki Szpital Kliniczny w Białymstoku (USK)",
    "Białystok",
    "SPZOZ",
    "https://uskwb.pl/",
    "Organ nadzorczy: Rada Społeczna"
))

HOSPITALS.append((
    "udsk",
    "Uniwersytecki Dziecięcy Szpital Kliniczny im. L. Zamenhofa (UDSK)",
    "Białystok",
    "SPZOZ",
    "https://szpitaldzieciecybialystok.sisco.info/",
    "Organ nadzorczy: Rada Społeczna"
))

HOSPITALS.append((
    "sniadeckiego",
    "SP ZOZ Wojewódzki Szpital Zespolony im. J. Śniadeckiego",
    "Białystok",
    "SPZOZ",
    "https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html",
    "Organ tworzący: Województwo Podlaskie. Organ nadzorczy: Rada Społeczna"
))

HOSPITALS.append((
    "bco",
    "Białostockie Centrum Onkologii im. M. Skłodowskiej-Curie",
    "Białystok",
    "SPZOZ",
    "https://www.onkologia.bialystok.pl/",
    "Organ tworzący: Województwo Podlaskie. Rada Społeczna powołana Uchwałą Nr 72/1442/2025"
))

HOSPITALS.append((
    "mswia",
    "SP ZOZ MSWiA w Białymstoku im. M. Zyndrama-Kościałkowskiego",
    "Białystok",
    "SPZOZ",
    "https://bip.zozmswia.bialystok.pl/",
    "Organ tworzący: Minister Spraw Wewnętrznych i Administracji"
))

HOSPITALS.append((
    "choroszcz",
    "SP Psychiatryczny ZOZ im. dr. St. Deresza",
    "Choroszcz",
    "SP Psychiatryczny ZOZ",
    "https://bip-sppzozc.podlaskie.eu/",
    "Organ tworzący: Województwo Podlaskie"
))

HOSPITALS.append((
    "lomza",
    "Szpital Wojewódzki im. Kardynała Stefana Wyszyńskiego",
    "Łomża",
    "SPZOZ",
    "https://www.szpital-lomza.pl/",
    "Organ tworzący: Samorząd Województwa Podlaskiego"
))

HOSPITALS.append((
    "suwalki_rundyger",
    "Szpital Wojewódzki im. dr. Ludwika Rydygiera",
    "Suwałki",
    "SPZOZ",
    "https://bip-spsws.podlaskie.eu/",
    "Organ tworzący: Województwo Podlaskie"
))

HOSPITALS.append((
    "suwalki_psych",
    "Specjalistyczny Psychiatryczny SP ZOZ",
    "Suwałki",
    "SPZOZ",
    "https://bip-spspzozs.podlaskie.eu/",
    "Organ tworzący: Województwo Podlaskie"
))

# === CZESC II — Szpitale powiatowe ===

HOSPITALS.append((
    "augustow",
    "SP ZOZ",
    "Augustów",
    "SPZOZ",
    "https://bip-spzoz-staugustow.podlaskie.eu/",
    "Organ tworzący: Powiat Augustowski"
))

HOSPITALS.append((
    "bielsk",
    "SP ZOZ",
    "Bielsk Podlaski",
    "SPZOZ",
    "https://www.spzoz-bielsk.pl/",
    "Organ tworzący: Powiat Bielski"
))

HOSPITALS.append((
    "hajnowka",
    "SP ZOZ",
    "Hajnówka",
    "SPZOZ",
    "https://spzozhajnowka.pl/",
    "Organ tworzący: Powiat Hajnowski"
))

HOSPITALS.append((
    "siemiatycze",
    "SP ZOZ (Szpital Powiatowy)",
    "Siemiatycze",
    "SPZOZ",
    "https://spzozsiemiatycze.pl/",
    "Organ tworzący: Powiat Siemiatycki"
))

HOSPITALS.append((
    "sokolka",
    "SP ZOZ",
    "Sokółka",
    "SPZOZ",
    "https://szpitalsokolka.pl/",
    "Organ tworzący: Powiat Sokólski"
))

HOSPITALS.append((
    "monki",
    "SP ZOZ",
    "Mońki",
    "SPZOZ",
    "https://spzoz.szpital-monki.h2.pl/",
    "Organ tworzący: Powiat Moniecki"
))

HOSPITALS.append((
    "grajewo",
    "Szpital Ogólny im. dr. Witolda Ginela",
    "Grajewo",
    "SPZOZ",
    "https://www.szpital-grajewo.pl/",
    "Organ tworzący: Powiat Grajewski"
))

HOSPITALS.append((
    "kolno",
    "Szpital Ogólny w Kolnie",
    "Kolno",
    "SPZOZ",
    "https://www.szpitalkolno.pl/",
    "Organ tworzący: Powiat Kolneński"
))

HOSPITALS.append((
    "wysokie",
    "Szpital Ogólny w Wysokiem Mazowieckiem",
    "Wysokie Mazowieckie",
    "SPZOZ",
    "https://szpitalwysmaz.pl/",
    "Organ tworzący: Powiat Wysokomazowiecki"
))

HOSPITALS.append((
    "sejny",
    "SP ZOZ (Szpital Powiatowy)",
    "Sejny",
    "SPZOZ",
    "https://szpital.sejny.pl/",
    "Organ tworzący: Powiat Sejneński"
))

HOSPITALS.append((
    "zambrow",
    "Szpital Powiatowy w Zambrowie sp. z o.o.",
    "Zambrow",
    "sp. z o.o.",
    "",
    "Jedyny szpital jako spółka handlowa. Organ: Rada Nadzorcza"
))


def person_key(hospital_id: str, name: str, role: str) -> str:
    """Deterministic key to detect duplicates within hospital+role."""
    raw = f"{hospital_id}:{name}:{role}"
    return hash(raw)


def build_data():
    """
    Returns (hospitals, persons_list, roles_list) from the embedded data.
    Deduplicates persons by normalised name across all roles/hospitals.
    """
    persons_map = {}  # norm_name -> person_uid -> Person
    roles = []
    hospitals = []

    # Part-of-speech helper (for KWW/local committees and "brak danych" entries)
    def is_kww_local(raw: str) -> bool:
        return raw.startswith("KWW") or raw.startswith("KW") or "KWW" in raw or "KW " in raw or "komitet" in raw.lower()

    def is_uncertain(raw: str) -> bool:
        return raw in ("brak/niepotwierdzona", "brak danych", "NIEZWERYFIKOWANE", "brak/niepotwierdzona", "niepotwierdzona", "brak potwierdzonej afiliacji", "komitet niepotwierdzony", "no confirmed affiliation")

    # ---- Register a person and optionally link a party ----
    def add_person(party_raw: str) -> list[Person]:
        """Normalize the party string, look up Person, link Party. Returns list of matched persons."""
        party_display, party_slug = normalise_party(party_raw)
        persons = [p for p in persons_by_name.values()]
        return persons

    # ---- Helper to register person ----
    def get_person(name: str, party: str = "", source_url: str = "", note: str = "") -> Person:
        n = norm_name(name)
        if n not in persons_map:
            persons_map[n] = {}
        # Check exact name match first
        for uid, p in persons_map[n].items():
            if p.name == name:
                if source_url and source_url not in p.source_urls:
                    p.source_urls.append(source_url)
                # If party is uncertain, keep it as note; if it's a KWW or real party, add slug
                if party and not is_uncertain(party) and not party.startswith("histor"):
                    _, slug = normalise_party(party)
                    if slug and slug not in p.party_slugs:
                        p.party_slugs.append(slug)
                else:
                    if party and not is_uncertain(party) and not party.startswith("histor"):
                        p.party_note = party
                    elif is_uncertain(party) and (not p.party_note or p.party_note == "brak/niepotwierdzona"):
                        p.party_note = party
                return p
        uid = str(uuid.uuid4())
        p = Person(uid=uid, name=name, party_note=party, source_urls=[] if not source_url else [source_url], note=note)
        if party and not is_uncertain(party) and not party.startswith("histor"):
            _, slug = normalise_party(party)
            if slug:
                p.party_slugs.append(slug)
        persons_map[n][uid] = p
        return p

    # =========================================================================
    # 1. Uniwersytecki Szpital Kliniczny w Białymstoku (USK)
    # =========================================================================
    src_dyr = "https://uskwb.pl/dyrekcja-szpitala/"
    src_rada = "https://uskbialystok.sisco.info/wladze,36_8"
    hospitals.append(Hospital(id="uskwb", name="Uniwersytecki Szpital Kliniczny w Białymstoku (USK)",
                              city="Białystok", legal_form="SPZOZ", website=src_dyr))
    roles.append(Role("uskwb", get_person("prof. dr hab. n. med. Jan Kochanowicz", party="brak/niepotwierdzona", source_url=src_dyr), "DYREKTOR", "Dyrektor Naczelny"))
    roles.append(Role("uskwb", get_person("dr hab. n. med. Marzena Wojewódzka-Żelezniakowicz", party="brak/niepotwierdzona", source_url=src_dyr), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("uskwb", get_person("mgr Beata Kropiewnicka", party="brak/niepotwierdzona", source_url=src_dyr), "Z_CA", "Z-ca ds. Finansowych / Gł. Księgowy"))
    roles.append(Role("uskwb", get_person("mgr Piotr Kołos", party="brak/niepotwierdzona", source_url=src_dyr), "Z_CA", "Z-ca ds. Techniczno-Inwestycyjnych"))
    roles.append(Role("uskwb", get_person("prof. dr hab. Dariusz Lebensztejn", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("uskwb", get_person("prof. dr hab. Wojciech Miltyk", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "Z-ca Przewodniczącego"))
    roles.append(Role("uskwb", get_person("dr hab. Joanna Reszeć-Giełażyn", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "Sekretarz"))
    roles.append(Role("uskwb", get_person("Bartosz Niebrzydowski", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("Marcin Borsuk", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("Marek Malinowski", party="Wicemarszałek woj.; koalicja Obywatelska; niezrzeszony", source_url=src_rada, note="do potwierdzenia"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("Henryk Grzesiak", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("Elżbieta Sienkiewicz", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("prof. dr hab. Sławomir Terlikowski", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("uskwb", get_person("prof. dr hab. Jerzy Pałka", party="brak/niepotwierdzona", source_url=src_rada), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 2. UDSK
    # =========================================================================
    src_udsk = "https://szpitaldzieciecybialystok.sisco.info/?id=316"
    src_udsk_rada = "https://szpitaldzieciecybialystok.sisco.info/?id=416"
    hospitals.append(Hospital(id="udsk", name="Uniwersytecki Dziecięcy Szpital Kliniczny im. L. Zamenhofa",
                              city="Białystok", legal_form="SPZOZ", website=src_udsk))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Anna Wasilewska", party="brak/niepotwierdzona", source_url=src_udsk), "DYREKTOR", "Dyrektor"))
    roles.append(Role("udsk", get_person("dr n. o zdr. Cezary Rzemek", party="brak/niepotwierdzona", source_url=src_udsk), "Z_CA", "Z-ca ds. Ekonomicznych"))
    roles.append(Role("udsk", get_person("mgr inż. Wojciech Roszkowski", party="brak/niepotwierdzona", source_url=src_udsk), "Z_CA", "Z-ca ds. Administr.-Techn."))
    roles.append(Role("udsk", get_person("Elżbieta Sienkiewicz", party="brak/niepotwierdzona", source_url=src_udsk), "Z_CA", "Z-ca ds. Pielęgniarstwa"))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Beata Naumnik", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "Przewodnicząca"))
    roles.append(Role("udsk", get_person("mgr Sylwia Ignaczak-Piotrowska", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("mgr Katarzyna Bardłowska", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("mgr Monika Anna Borkowska", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("dr n. med. Ryta Filipkowska", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("mgr Bożena Kostro", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Anna Niemcunowicz-Janica", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Elżbieta Tryniszewska", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Halina Car", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("udsk", get_person("prof. dr hab. n. med. Jarosław Daniluk", party="brak/niepotwierdzona", source_url=src_udsk_rada), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 3. Śniadeckiego
    # =========================================================================
    src_sn = "https://podlaskie.eu/zdrowie/bogdan-kalicki-oficjalnie-objal-stanowisko-dyrektora-sniadecji.html"
    src_sn_rada = "https://bip-spzozwszjs.podlaskie.eu/rada.html"
    hospitals.append(Hospital(id="sniaskiego", name="SP ZOZ Wojewódzki Szpital Zespolony im. J. Śniadeckiego",
                              city="Białystok", legal_form="SPZOZ", website=src_sn))
    roles.append(Role("sniaskiego", get_person("Bogdan Kalicki", party="brak/niepotwierdzona", source_url=src_sn), "DYREKTOR", "Dyrektor (p.o.)"))
    roles.append(Role("sniaskiego", get_person("Karol Pilecki", party="Koalicja Obywatelska", source_url=src_sn_rada, note="radny Sejmiku; historycznie PSL"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("sniaskiego", get_person("Joanna Sosnowska", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Bartosz Czaczkowski", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Bogusław Dębski", party="Prawica Rzeczypospolitej", source_url=src_sn_rada, note="radny Sejmiku; historycznie PSL"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Jarosław Zygmunt Dworzański", party="Koalicja Obywatelska", source_url=src_sn_rada, note="radny Sejmiku; b. marszałek woj."), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Henryk Gryko", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Paweł Jamróz", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Jan Maksymilian Janiszewski", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Katarzyna Kluczyk-Ceckowska", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Rafał Kołakowski", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Wojciech Łuczaj", party="brak danych", source_url=src_sn_rada, note="możliwie Łuczaj-Pogorzelski, PSL — NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Tomasz Stefan Madras", party="Prawo i Sprawiedliwość", source_url=src_sn_rada, note="radny Sejmiku; b. wicewojewoda"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Jarosław Matwiejuk", party="historycznie SLD/Lewica", source_url=src_sn_rada, note="b. poseł SLD — NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Andrzej Parafiniuk", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Lech Rutkowski", party="brak danych", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Wojciech Strzałkowski", party="brak danych", source_url=src_sn_rada, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sniaskiego", get_person("Irina Kowalska", party="brak/niepotwierdzona", source_url=src_sn_rada), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 4. Białostockie Centrum Onkologii
    # =========================================================================
    src_bco = "https://bip-bco.podlaskie.eu/wadze_i_struktura_organizacyjna/kierownictwo-szpitala.html"
    src_bco_rada = "https://www.onkologia.bialystok.pl/pages/rada-spoleczna"
    hospitals.append(Hospital(id="bco", name="Białostockie Centrum Onkologii im. M. Skłodowskiej-Curie",
                              city="Białystok", legal_form="SPZOZ", website=src_bco))
    roles.append(Role("bco", get_person("dr n. o zdr. Magdalena Joanna Borkowska", party="brak/niepotwierdzona", source_url=src_bco), "DYREKTOR", "Dyrektor"))
    roles.append(Role("bco", get_person("dr n. med. Dorota Elżbieta Kazberuk", party="brak/niepotwierdzona", source_url=src_bco), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("bco", get_person("dr n. o zdr. Elżbieta Potentas", party="brak/niepotwierdzona", source_url=src_bco), "Z_CA", "Z-ca ds. Koordynacji Onkologicznej"))
    roles.append(Role("bco", get_person("Anna Augustyn", party="Koalicja Obywatelska", source_url=src_bco_rada, note="radna Sejmiku"), "RADA_SPOLECZNA", "Przewodnicząca"))
    roles.append(Role("bco", get_person("Cezary Cieślukowski", party="PSL", source_url=src_bco_rada, note="Przewodniczący Sejmiku VII kad."), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Jarosław Zygmunt Dworzański", party="Koalicja Obywatelska", source_url=src_bco_rada, note="radny Sejmiku"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Bogdan Dyjuk", party="PSL", source_url=src_bco_rada, note="radny Sejmiku; czł. Zarządu Woj."), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Janusz Dzięcioł", party="NIEZWERYFIKOWANE", source_url=src_bco_rada, note="inna osoba niż zmarły poseł PO; afiliacji nie potwierdzono"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Ewa Feszler", party="brak danych", source_url=src_bco_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Robert Jabłoński", party="Prawo i Sprawiedliwość", source_url=src_bco_rada, note="prawdopodobnie PiS; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Leszek Maciej Lulewicz", party="brak danych", source_url=src_bco_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Jacek Piorunek", party="Koalicja Obywatelska", source_url=src_bco_rada, note="radny Sejmiku"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Agnieszka Zaręba", party="brak danych", source_url=src_bco_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Agnieszka Zawistowska", party="brak danych", source_url=src_bco_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bco", get_person("Maciej Żywno", party="Polska 2050", source_url="https://www.senat.gov.pl/sklad/senatorowie/senator,1072,11,maciej-zywno.html", note="senator, wicemarszałek Senatu"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 5. MSWiA
    # =========================================================================
    src_mswia = "https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna"
    src_mswia_rada = "https://bip.zozmswia.bialystok.pl/wladze-i-struktura-organizacyjna/rada-spoleczna/"
    hospitals.append(Hospital(id="mswia", name="SP ZOZ MSWiA w Białymstoku im. M. Zyndrama-Kościałkowskiego",
                              city="Białystok", legal_form="SPZOZ", website=src_mswia))
    roles.append(Role("mswia", get_person("Marek Stanisław Karp", party="brak/niepotwierdzona", source_url=src_mswia, note="p.o. od 20.02.2024"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("mswia", get_person("Daniel Szutko", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("mswia", get_person("mgr inż. Sławomir Wilczewski", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "Z-ca Przewodniczącego (Policja)"))
    roles.append(Role("mswia", get_person("st. bryg. Marcin Wierel", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (PSP)"))
    roles.append(Role("mswia", get_person("płk SG Marek Sochański", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (Straż Graniczna)"))
    roles.append(Role("mswia", get_person("ppłk Małgorzata Błażewicz", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (Służba Więzienna)"))
    roles.append(Role("mswia", get_person("dr n. med. Anatol Aksiucik", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (Naczelna Rada Lekarska)"))
    roles.append(Role("mswia", get_person("dr n. med. Beata Olejnik", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (NRPiP)"))
    roles.append(Role("mswia", get_person("prof. dr hab. n. med. Andrzej Dąbrowski", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (UMB)"))
    roles.append(Role("mswia", get_person("Krzysztof Turowski", party="brak/niepotwierdzona", source_url=src_mswia_rada), "RADA_SPOLECZNA", "członek (MSWiA)"))

    # =========================================================================
    # 6. Choroszcz
    # =========================================================================
    src_ch = "https://bip-sppzozc.podlaskie.eu/"
    hospitals.append(Hospital(id="choroszcz", name="SP Psychiatryczny ZOZ im. dr. St. Deresza",
                              city="Choroszcz", legal_form="SP Psychiatryczny ZOZ", website=src_ch))
    roles.append(Role("choroszcz", get_person("Alicja Skindzielewska", party="brak/niepotwierdzona", source_url=src_ch, note="p.o. Dyrektora"), "DYREKTOR", "p.o. Dyrektora"))
    roles.append(Role("choroszcz", get_person("Andrzej Szewczuk", party="brak/niepotwierdzona", source_url=src_ch, note="dyrektor-elekt od VIII.2026"), "DYREKTOR", "Dyrektor-elekt (od VIII.2026)"))
    roles.append(Role("choroszcz", get_person("lek. Justyna Anna Piłasiewicz", party="brak/niepotwierdzona", source_url=src_ch), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("choroszcz", get_person("Ewelina Cecotka-Białokozowicz", party="brak/niepotwierdzona", source_url=src_ch), "Z_CA", "Gł. Księgowa"))
    roles.append(Role("choroszcz", get_person("dr n. med. Nadzieja Sołowiej", party="brak/niepotwierdzona", source_url=src_ch), "Z_CA", "Naczelna Pielęgniarka"))
    roles.append(Role("choroszcz", get_person("Monika Miklaszewicz", party="brak danych", source_url=src_ch), "RADA_SPOLECZNA", "Przewodnicząca"))

    # =========================================================================
    # 7. Łomża
    # =========================================================================
    src_lomza = "https://www.szpital-lomza.pl/"
    src_lomza_rada = "https://www.szpital-lomza.pl/index.php/wiadomosc/rada-spoleczna"
    src_lomza_dyr = "https://www.rynekzdrowia.pl/.../Nowy-dyrektor-w-Szpitalu-Wojewodzkim-w-Lomzy,269250,1.html"
    hospitals.append(Hospital(id="lomza", name="Szpital Wojewódzki im. Kardynała Stefana Wyszyńskiego",
                              city="Łomża", legal_form="SPZOZ", website=src_lomza))
    roles.append(Role("lomza", get_person("Dariusz Domasiewicz", party="KWW Przyjazna Łomża", source_url=src_lomza_dyr, note="Radny Rady Miejskiej Łomży"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("lomza", get_person("Maria Konopka", party="Koalicja Obywatelska", source_url=src_lomza_rada, note="kandydatka PO do sejmiku 2024"), "RADA_SPOLECZNA", "Przewodnicząca"))
    roles.append(Role("lomza", get_person("Marek Olbryś", party="PiS", source_url="https://www.portalsamorzadowy.pl/osoba/marek-olbrys,876.html", note="wicemarszałek woj. / radny Sejmiku"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Jacek Piorunek", party="Koalicja Obywatelska", source_url="https://www.portalsamorzadowy.pl/osoba/jacek-piorunek,6388.html", note="radny Sejmiku"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Bernadeta Krynicka", party="PiS", source_url="https://pl.wikipedia.org/wiki/Bernadeta_Krynicka", note="radna Sejmiku; b. posłanka PiS"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Lech Marek Szabłowski", party="PiS", source_url="https://www.portalsamorzadowy.pl/osoba/lech-szablowski,55269.html", note="starosta łomżyński / radny powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Krzysztof Ryszard Kozicki", party="PiS", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3469307", note="wójt Piątnicy"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Andrzej Borusiewicz", party="brak potwierdzonej afiliacji", source_url="https://pl.wikipedia.org/wiki/Andrzej_Borusiewicz", note="prorektor MANS w Łomży, wiceprezes ARiMR"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Karol Kossakowski", party="niepotwierdzona", source_url=src_lomza_rada, note="kandydat do sejmiku; rej. Zambrów"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Stanisław Kossakowski", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Andrzej Szymański", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Anna Malwina Szynkiewicz", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Dorota Waszkiewicz", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Renata Szydłowska", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Milena Siok", party="brak danych", source_url=src_lomza_rada, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Marcin Biedrzycki", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("lomza", get_person("Janusz Józef Cieciórski", party="brak danych", source_url=src_lomza_rada), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 8. Suwałki — Rydygiera
    # =========================================================================
    src_sw = "https://bip-spsws.podlaskie.eu/6b54de5acb7.html"
    src_sw_rada1 = "https://samorzad.podlaskie.eu/radni/naszkiewicz-anna.html"
    src_sw_rada2 = "https://www.portalsamorzadowy.pl/osoba/bogdan-dyjuk,2177.html"
    hospitals.append(Hospital(id="suwalki_rundyger", name="Szpital Wojewódzki im. dr. Ludwika Rydygiera",
                              city="Suwałki", legal_form="SPZOZ", website=src_sw))
    roles.append(Role("suwalki_rundyger", get_person("Adam Szałanda", party="brak/niepotwierdzona", source_url=src_sw), "DYREKTOR", "Dyrektor"))
    roles.append(Role("suwalki_rundyger", get_person("lek. Włodzimierz Żaworonok", party="brak danych", source_url=src_sw), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("suwalki_rundyger", get_person("Agnieszka Kapczyńska", party="brak danych", source_url=src_sw), "Z_CA", "Gł. Księgowy"))
    roles.append(Role("suwalki_rundyger", get_person("Anna Naszkiewicz", party="Koalicja Obywatelska", source_url=src_sw_rada1, note="radna Sejmiku; b. wicemarszałek"), "RADA_SPOLECZNA", "Przewodnicząca"))
    roles.append(Role("suwalki_rundyger", get_person("Bogdan Dyjuk", party="Trzecia Droga (PSL–Polska 2050)", source_url=src_sw_rada2, note="radny Sejmiku, czł. Zarządu Woj."), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Tadeusz Buchowiecki", party="brak/niepotwierdzona", source_url=src_sw, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Wojciech Wasilewski", party="brak/niepotwierdzona", source_url=src_sw, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Marta Żybura", party="Koalicja Obywatelska", source_url=src_sw, note="kandydatka KO do Rady Miejskiej 2024; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Dariusz Bogdan", party="brak/niepotwierdzona", source_url="https://pl.wikipedia.org/wiki/Dariusz_Bogdan", note="b. wiceminister gospodarki; przynależność partyjna niepotwierdzona"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Joanna Jędzul", party="KWW Razem dla Suwałk", source_url=src_sw), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Dorota Iwanowicz", party="Trzecia Droga", source_url="https://www.farmer.pl/wybory/osoba/dorota-iwanowicz,221972.html", note="radna gm. Suwałki; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Daniel Leszek Makarewicz", party="KWW Suwalskie Porozumienie Prawicy", source_url=src_sw, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Elżbieta Luto", party="brak/niepotwierdzona", source_url=src_sw), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Justyna Drażba", party="brak/niepotwierdzona", source_url=src_sw), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Grzegorz Paweł Matys", party="brak/niepotwierdzona", source_url=src_sw), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_rundyger", get_person("Krystian Małyszko", party="brak/niepotwierdzona", source_url=src_sw), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 9. Suwałki — Psychiatryczny
    # =========================================================================
    src_spp_s = "https://bip-spspzozs.podlaskie.eu/"
    src_spp_s_rada = "https://bip-spspzozs.podlaskie.eu/rada_spoleczna.html"
    hospitals.append(Hospital(id="suwalki_psych", name="Specjalistyczny Psychiatryczny SP ZOZ",
                              city="Suwałki", legal_form="SPZOZ", website=src_spp_s))
    roles.append(Role("suwalki_psych", get_person("Bożena Łapińska", party="brak/niepotwierdzona", source_url=src_spp_s), "DYREKTOR", "Dyrektor"))
    roles.append(Role("suwalki_psych", get_person("lek. Michał Naczas", party="brak danych", source_url=src_spp_s), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("suwalki_psych", get_person("Marek Wasilewski", party="brak danych", source_url=src_spp_s), "Z_CA", "Gł. Księgowy"))
    roles.append(Role("suwalki_psych", get_person("Władysław Renowicki", party="brak/niepotwierdzona", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("suwalki_psych", get_person("Eliza Kupich", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Waldemar Kwaterski", party="Koalicja Obywatelska", source_url=src_spp_s_rada, note="radny Sejmiku; dyr. SP ZOZ Sejny; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Jacek Niedźwiedzki", party="KO/PO", source_url=src_spp_s_rada, note="poseł z Suwałk; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Elżbieta Baranowska", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Natalia Bogdan-Czarnowska", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Anna Maria Gawlińska", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Anna Kumiszcza", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Tomasz Romualdowski", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Bożena Jadwiga Chmielewska", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Dorota Iwanowicz", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Sławomir Jan Szeszko", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("suwalki_psych", get_person("Aneta Jolanta Szmidt", party="brak danych", source_url=src_spp_s_rada), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 10. Augustów
    # =========================================================================
    src_a = "https://bip-spzoz-staugustow.podlaskie.eu/organy_wadze/kierownictwo_szpitala/"
    src_a_rada = "https://bip-staugustow.podlaskie.eu/resource/116181/14.pdf/attachment.1"
    src_a_dziennik = "https://www.dziennikpowiatowy.pl/artykul/13664"
    hospitals.append(Hospital(id="augustow", name="SP ZOZ", city="Augustów",
                              legal_form="SPZOZ", website=src_a))
    roles.append(Role("augustow", get_person("Adam Dębski", party="brak/niepotwierdzona", source_url=src_a, note="dyrektor od 1.09.2024"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("augustow", get_person("Wioletta Tomaszycka-Bednarczyk", party="brak danych", source_url=src_a), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("augustow", get_person("Piotr Rusiecki", party="PiS", source_url=src_a_rada, note="starosta augustowski, radny powiatu"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("augustow", get_person("Marek Dobkowski", party="KWW Samorząd Powiatu Augustowskiego", source_url=src_a_dziennik), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("augustow", get_person("Waldemar Jedliński", party="PiS", source_url=src_a_dziennik, note="członek Zarządu Powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("augustow", get_person("Elżbieta Puczyłowska", party="PiS", source_url=src_a_dziennik, note="wiczeprzewodnicząca Rady Powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("augustow", get_person("Alicja Dobrowolska", party="PiS", source_url=src_a_rada, note="członek Zarządu Powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("augustow", get_person("Dariusz Jan Szkiłądź", party="KWW BAU 2024", source_url=src_a_dziennik), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("augustow", get_person("Jadwiga Józefa Milewska", party="brak danych", source_url=src_a_rada, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 11. Bielsk Podlaski
    # =========================================================================
    src_b = "https://www.spzoz-bielsk.pl/o-nas/dyrekcja/"
    src_b_portal = "https://www.portalsamorzadowy.pl/powiat/bielski,295.html"
    src_b_rada = "https://samorzad.gov.pl/web/powiat-bielski/ii-sesja-rady-powiatu-w-bielsku-podlaskim"
    hospitals.append(Hospital(id="bielsk", name="SP ZOZ", city="Bielsk Podlaski",
                              legal_form="SPZOZ", website=src_b))
    roles.append(Role("bielsk", get_person("Piotr Selwesiuk", party="KW Koalicja Bielska", source_url=src_b, note="wypowiedzenie z 29.06.2026"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("bielsk", get_person("dr n. med. Arsalan Azzaddin", party="brak/niepotwierdzona", source_url=src_b), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("bielsk", get_person("Piotr Bożko", party="KW Koalicja Bielska", source_url=src_b_rada, note="wicestarosta, radny powiatu"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("bielsk", get_person("Elżbieta Fionik", party="KW Koalicja Bielska", source_url=src_b_portal, note="radna powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Maria Ryżyk", party="KW Koalicja Bielska", source_url=src_b_portal, note="radna powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Andrzej Leszczyński", party="KWW Nasze Podlasie", source_url=src_b_portal, note="Przew. Rady Powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Adam Łęczycki", party="KWW Nasze Podlasie", source_url=src_b_portal, note="radny powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Zbigniew Jan Szpakowski", party="KWW Nasze Podlasie", source_url=src_b_portal, note="radny powiatu, czł. Zarządu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Jan Niewiński", party="PiS", source_url="https://www.portalsamorzadowy.pl/osoba/jan-niewinski,48951.html", note="radny powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("bielsk", get_person("Walentyna Tyszko-Dubow", party="NIEZWERYFIKOWANE", source_url=src_b_rada, note="PO/KO z Hajnówki"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 12. Hajnówka
    # =========================================================================
    src_h = "https://spzozhajnowka.pl/strona-3343-dyrekcja.html"
    src_h_rada = "Uchwała III/13/2024 Rady Powiatu Hajnowskiego"
    src_h_portal = "https://www.portalsamorzadowy.pl/"
    hospitals.append(Hospital(id="hajnowka", name="SP ZOZ", city="Hajnówka",
                              legal_form="SPZOZ", website=src_h))
    roles.append(Role("hajnowka", get_person("Grzegorz Tomaszuk", party="brak/niepotwierdzona", source_url=src_h), "DYREKTOR", "Dyrektor"))
    roles.append(Role("hajnowka", get_person("Tomasz Musiuk", party="brak/niepotwierdzona", source_url=src_h), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("hajnowka", get_person("Andrzej Skiepko", party="KWW Ziemia Hajnowska", source_url=src_h_portal + "osoba/andrzej-skiepko,17764.html", note="Źródła rozbieżne: KWW Ziemia Hajnowska lub KWW Porozumienie Samorządowe"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("hajnowka", get_person("Jerzy Sirak", party="NIEZWERYFIKOWANE", source_url=src_h_portal + "osoba/jerzy-sirak,1302.html", note="rozbieżne: KO vs historycznie SLD; b. burmistrz"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Justyna Jarmocik", party="Trzecia Droga (PSL / Polska 2050)", source_url=src_h_portal + "powiat/hajnowski,350.html", note="Wicestarosta"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Albert Waldemar Litwinowicz", party="komitet niepotwierdzony", source_url=src_h_rada, note="wójt Białowieży"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Jerzy Wasiluk", party="możliwie Trzecia Droga", source_url=src_h_rada, note="wójt Czyż; do potwierdzenia"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Leon Małaszewski", party="KWW Ziemia Hajnowska", source_url=src_h_rada, note="wójt; niepotwierdzone partyjnie"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Lucyna Smoktunowicz", party="KWW Nasza Przyszłość; bezpartyjna", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3412858", note="wójt"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Ireneusz Roman Kiendyś", party="KWW Hajnówka Razem; bezpartyjny", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3459046", note="burmistrz"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Mirosław Markiewicz", party="KWW Roztropna Troska o Dobro Gminy", source_url="https://w.radio.bialystok.pl/wiadomosci/index/id/236847", note="burmistrz Kleszczele"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Aneta Leonowicz", party="KWW Narew Odnowa", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3588266", note="wójt"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("hajnowka", get_person("Jarosław Gołubowski", party="KWW Wspólna Narewka", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3421407", note="wójt"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 13. Siemiatycze
    # =========================================================================
    src_sm = "https://spzozsiemiatycze.pl/?page_id=395"
    src_sm_radio = "https://www.radio.bialystok.pl/wiadomosci/index/id/238459"
    src_sm_siem = "https://siemiatycze.podlasie24.pl/region/zarzad-powiatu-siemiatyckiego-z-absolutorium-za-rok-2023-20240615102143"
    src_sm_portal = "https://www.portalsamorzadowy.pl/"
    hospitals.append(Hospital(id="siemiatycze", name="SP ZOZ (Szpital Powiatowy)", city="Siemiatycze",
                              legal_form="SPZOZ", website=src_sm))
    roles.append(Role("siemiatycze", get_person("Andrzej Szewczuk", party="brak/niepotwierdzona", source_url=src_sm, note="przejście do Choroszczy od VIII.2026"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("siemiatycze", get_person("Lech Zenon Hackiewicz", party="brak/niepotwierdzona", source_url=src_sm), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("siemiatycze", get_person("Mariusz Cieślik", party="PSL", source_url=src_sm_radio, note="wybrany z KWW Ruch Ludowy"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("siemiatycze", get_person("Sylwester Zgierun", party="PSL", source_url=src_sm_siem, note="KWW Ruch Ludowy"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("siemiatycze", get_person("Mariusz Pyzowski", party="Koalicja Obywatelska", source_url=src_sm_portal + "osoba/mariusz-pyzowski,34169.html", note="Przew. Rady Powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("siemiatycze", get_person("Jan Żeruń", party="KWW SPW", source_url=src_sm_siem.replace("2023-20240615102143", "")), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("siemiatycze", get_person("Edward Krasowski", party="KWW własny", source_url="https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739772", note="wójt; afiliacja niepotwierdzona"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("siemiatycze", get_person("Piotr Siniakowicz", party="PO/KO", source_url="https://pl.wikipedia.org/wiki/Piotr_Siniakowicz", note="KWW Dla Siemiatycz Razem"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 14. Sokółka
    # =========================================================================
    src_sk = "https://szpitalsokolka.pl/dyrekcja/"
    src_sk_radio = "https://www.radio.bialystok.pl/wiadomosci/index/id/228040"
    src_sk_pkw = "https://samorzad2024.pkw.gov.pl/samorzad2024/pl/rada_powiatu/kandydat/3296821"
    src_sk_radio2 = "https://www.radio.bialystok.pl/wiadomosci/index/id/237569"
    hospitals.append(Hospital(id="sokolka", name="SP ZOZ", city="Sokółka",
                              legal_form="SPZOZ", website=src_sk))
    roles.append(Role("sokolka", get_person("Anna Marta Aniśkiewicz", party="niepotwierdzona", source_url=src_sk_radio, note="KWW Razem dla Sokółki"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("sokolka", get_person("Adam Pietruczuk", party="brak/niepotwierdzona", source_url=src_sk), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("sokolka", get_person("Maria Julita Budrowska", party="brak/niepotwierdzona", source_url=src_sk), "Z_CA", "Z-ca ds. Ekonomicznych"))
    roles.append(Role("sokolka", get_person("Piotr Rećko", party="PiS", source_url=src_sk_pkw, note="starosta sokólski, radny powiatu"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("sokolka", get_person("Cezary Możejko", party="NIEZWERYFIKOWANE", source_url=src_sk, note="możliwie wójt gm. Sidra; zbieżność nazwisk"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("sokolka", get_person("Krzysztof Pawłowski", party="PiS", source_url=src_sk_radio2, note="radny powiatu, wiceprzew. Rady Powiatu"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 15. Mońki
    # =========================================================================
    src_m = "https://spzoz.szpital-monki.h2.pl/kadra-zarzadzajaca/"
    src_m_portal = "https://www.portalsamorzadowy.pl/powiat/moniecki,362.html"
    hospitals.append(Hospital(id="monki", name="SP ZOZ", city="Mońki",
                              legal_form="SPZOZ", website=src_m))
    roles.append(Role("monki", get_person("lek. med. Sebastian Wysocki", party="brak/niepotwierdzona", source_url=src_m), "DYREKTOR", "Dyrektor"))
    roles.append(Role("monki", get_person("lek. med. Łucja Chojnowska", party="brak/niepotwierdzona", source_url=src_m, note="p.o. Z-ca ds. Lecznictwa"), "Z_CA", "Z-ca ds. Lecznictwa (p.o.)"))
    roles.append(Role("monki", get_person("mgr Halina Skibicka", party="brak/niepotwierdzona", source_url=src_m), "Z_CA", "Z-ca ds. Pielęgniarstwa"))
    roles.append(Role("monki", get_person("Barbara Lubecka", party="brak/niepotwierdzona", source_url=src_m, note="p.o. Gł. Księgowa"), "Z_CA", "Gł. Księgowa (p.o.)"))
    roles.append(Role("monki", get_person("Joanna Kitlas", party="KWW BMN Aktywny Samorząd", source_url=src_m_portal, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "Przewodnicząca"))

    # =========================================================================
    # 16. Grajewo
    # =========================================================================
    src_gr = "https://www.szpital-grajewo.pl/dyrekcja"
    src_gr_portal = "https://www.portalsamorzadowy.pl/powiat/grajewski,329.html"
    src_gr_biels = "https://bip.starostwograjewo.pl/612-kadencja-vii-2024-2029.html"
    src_gr_e = "https://e-grajewo.pl/wiadomosc,barbara-ciszewska--kandydatka-na-burmistrza-grajewa,54754.html"
    src_gr_grajewo = "https://www.grajewo.pl/pl/aktualnosci/maciej-pawel-bednarko-zaprzysiezony..."
    hospitals.append(Hospital(id="grajewo", name="Szpital Ogólny im. dr. Witolda Ginela",
                              city="Grajewo", legal_form="SPZOZ", website=src_gr))
    roles.append(Role("grajewo", get_person("mgr Marta Romanowska", party="brak/niepotwierdzona", source_url=src_gr), "DYREKTOR", "Dyrektor"))
    roles.append(Role("grajewo", get_person("lek. med. Adam Leszczyński", party="brak/niepotwierdzona", source_url=src_gr, note="p.o. Z-ca ds. Lecznictwa"), "Z_CA", "Z-ca ds. Lecznictwa (p.o.)"))
    roles.append(Role("grajewo", get_person("mgr Małgorzata Zdanewicz", party="brak/niepotwierdzona", source_url=src_gr), "Z_CA", "Gł. Księgowa"))
    roles.append(Role("grajewo", get_person("mgr Beata Szyszkowska", party="brak/niepotwierdzona", source_url=src_gr), "Z_CA", "Naczelna Pielęgniarka"))
    roles.append(Role("grajewo", get_person("Waldemar Remfeld", party="PiS", source_url=src_gr_portal, note="starosta grajewski, radny powiatu"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("grajewo", get_person("Milena Formejster", party="brak danych", source_url=src_gr), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("grajewo", get_person("Barbara Ciszewska", party="komitet 2024 niepotwierdzony", source_url=src_gr_e, note="b. radna powiatu; Stow. Nasza Przyszłość"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("grajewo", get_person("Maciej Paweł Bednarko", party="KWW Macieja Pawła Bednarko", source_url=src_gr_grajewo, note="burmistrz Grajewa"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("grajewo", get_person("Marek Kostrzewski", party="KWW Dobry Powiat", source_url=src_gr_biels, note="radny powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("grajewo", get_person("Jerzy Jan Nikliński", party="brak danych", source_url=src_gr, note="NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("grajewo", get_person("Bolesław Ruszczyk", party="PiS", source_url=src_gr_biels, note="radny powiatu"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 17. Kolno
    # =========================================================================
    src_kol = "https://www.szpitalkolno.pl/struktura/dyrekcja/"
    src_kol_portal = "https://www.portalsamorzadowy.pl/"
    hospitals.append(Hospital(id="kolno", name="Szpital Ogólny w Kolnie",
                              city="Kolno", legal_form="SPZOZ", website=src_kol))
    roles.append(Role("kolno", get_person("mgr Aneta Długozima", party="brak/niepotwierdzona", source_url=src_kol), "DYREKTOR", "Dyrektor"))
    roles.append(Role("kolno", get_person("lek. Oksana Dowlaszewicz", party="brak/niepotwierdzona", source_url=src_kol, note="p.o. Z-ca ds. Lecznictwa"), "Z_CA", "p.o. Z-ca ds. Lecznictwa"))
    roles.append(Role("kolno", get_person("mgr Izabela Górska", party="brak/niepotwierdzona", source_url=src_kol, note="p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy"), "Z_CA", "p.o. Z-ca ds. Ekon.-Fin. / Gł. Księgowy"))
    roles.append(Role("kolno", get_person("mgr Alicja Urbańska", party="brak/niepotwierdzona", source_url=src_kol), "Z_CA", "Przełożona Pielęgniarek"))
    roles.append(Role("kolno", get_person("Tadeusz Klama", party="KWW Porozumienie Samorządowe Powiatu Kolno", source_url=src_kol_portal + "osoba/tadeusz-klama,58658.html", note="radny powiatu"), "RADA_SPOLECZNA", "Przewodniczący"))
    roles.append(Role("kolno", get_person("Michał Kulczewski", party="KWW Porozumienie Samorządowe Powiatu Kolno", source_url=src_kol_portal + "osoba/michal-kulczewski,57069.html", note="radny powiatu"), "RADA_SPOLECZNA", "Wiceprzewodniczący"))
    roles.append(Role("kolno", get_person("Marek Samul", party="PO", source_url=src_kol, note="b. starosta kolneński; NIEZWERYFIKOWANE"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("kolno", get_person("Anita Krasińska", party="brak/niepotwierdzona", source_url=src_kol, note="kier. OPS Grabowo"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("kolno", get_person("Beata Helena Muniak", party="brak/niepotwierdzona", source_url=src_kol, note="Sekretarz Gminy"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("kolno", get_person("Robert Wacław Nadara", party="KWW Skuteczny i Przyjazny Samorząd", source_url=src_sk_radio2, note="radny powiatu"), "RADA_SPOLECZNA", "członek"))
    roles.append(Role("kolno", get_person("Mariusz Rakowski", party="PiS", source_url=src_kol_portal + "osoba/mariusz-rakowski,106463.html", note="radny Rady Miasta Kolno"), "RADA_SPOLECZNA", "członek"))

    # =========================================================================
    # 18. Wysokie Mazowieckie
    # =========================================================================
    src_wm = "https://szpitalwysmaz.pl/pl/onas"
    src_wm_portal = "https://www.portalsamorzadowy.pl/osoba/bogdan-zielinski,4307.html"
    src_wm_pkw = "https://samorzad2024.pkw.gov.pl/samorzad2024/pl/wbp/kandydat/3739309"
    hospitals.append(Hospital(id="wysokie", name="Szpital Ogólny w Wysokiem Mazowieckiem",
                              city="Wysokie Mazowieckie", legal_form="SPZOZ", website=src_wm))
    roles.append(Role("wysokie", get_person("mgr Wojciech Łuczaj", party="PiS", source_url=src_wm_pkw, note="kandydat na burmistrza Czyżewa 2024 z KW PiS"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("wysokie", get_person("lek. Julita Nikołajuk-Stasiuk", party="brak/niepotwierdzona", source_url=src_wm), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("wysokie", get_person("mgr Aleksandra Łapiak", party="brak/niepotwierdzona", source_url=src_wm), "Z_CA", "Z-ca ds. Ekonomicznych"))
    roles.append(Role("wysokie", get_person("mgr Anna Wądołkowska", party="brak/niepotwierdzona", source_url=src_wm), "Z_CA", "Przełożona Pielęgniarek"))
    roles.append(Role("wysokie", get_person("Bogdan Zieliński", party="PiS", source_url=src_wm_portal, note="starosta, radny powiatu — NIEZWERYFIKOWANE czy przewodniczy"), "RADA_SPOLECZNA", "Przewodniczący"))

    # =========================================================================
    # 19. Sejny
    # =========================================================================
    src_se = "https://szpital.sejny.pl/kadra"
    src_se_radio = "https://www.radio.bialystok.pl/gosc/suwalki/id/202941"
    src_se_suwalki = "https://www.suwalki24.pl/article/63,powiat-sejnenski-maciej-plesiewicz-nadal-starosta..."
    hospitals.append(Hospital(id="sejny", name="SP ZOZ (Szpital Powiatowy)",
                              city="Sejny", legal_form="SPZOZ", website=src_se))
    roles.append(Role("sejny", get_person("Waldemar Kwaterski", party="Koalicja Obywatelska", source_url=src_se_radio, note="radny Sejmiku od 2010, przew. Komisji Zdrowia"), "DYREKTOR", "Dyrektor"))
    roles.append(Role("sejny", get_person("Urszula Ordowska", party="brak/niepotwierdzona", source_url=src_se), "Z_CA", "Z-ca ds. Lecznictwa"))
    roles.append(Role("sejny", get_person("Maciej Plesiewicz", party="KW Ziemia Sejneńska", source_url=src_se_suwalki, note="starosta; NIEZWERYFIKOWANE czy przewodniczy"), "RADA_SPOLECZNA", "Przewodniczący"))

    # =========================================================================
    # 20. Zambrow — SPÓŁKA
    # =========================================================================
    src_zb = "https://aleo.com/int/company/szpital-powiatowy-w-zambrowie"
    src_zb_rada = "https://zambrowiacy.pl/info/bartosz-ziemianowicz/"
    hospitals.append(Hospital(id="zambrow", name="Szpital Powiatowy w Zambrowie sp. z o.o.",
                              city="Zambrow", legal_form="sp. z o.o.", website=src_zb,
                              notes="Jedyny szpital jako spółka handlowa. KRS 0000130730"))
    roles.append(Role("zambrow", get_person("Kamil Kowaleczko", party="brak/niepotwierdzona", source_url=src_zb, note="Prezes Zarządu od 16.07.2025"), "PREZES_ZARZADU", "Prezes Zarządu"))
    roles.append(Role("zambrow", get_person("Bartosz Rafał Ziemianowicz", party="NIEZWERYFIKOWANE", source_url=src_zb_rada, note="b. radny miasta; historycznie PO; PO vs PiS"), "RADA_NADZORCZA", "członek"))
    roles.append(Role("zambrow", get_person("Krzysztof Rodzik", party="brak danych", source_url=src_zb), "RADA_NADZORCZA", "członek"))
    roles.append(Role("zambrow", get_person("Michał Kulesza", party="brak danych", source_url=src_zb), "RADA_NADZORCZA", "członek"))
    roles.append(Role("zambrow", get_person("Patrycja Magdalena Bugaj", party="brak danych", source_url=src_zb), "RADA_NADZORCZA", "członek"))

    # =========================================================================
    # Deduplicate persons across all hospitals
    # =========================================================================
    persons = []
    party_set = {}  # canonical_name -> Party
    person_party_map = {}  # person name -> list of (display_name, slug)
    for uid_map in persons_map.values():
        for p in uid_map.values():
            persons.append(p)
            # Collect party slugs
            for slug in p.party_slugs:
                if slug not in party_set:
                    party_name, _ = normalise_party(slug)
                    party_set[slug] = Party(slug=slug, name=party_name)
            # Parse party_note for additional parties
            if p.party_note and p.party_note not in ("brak/niepotwierdzona", "brak danych", "NIEZWERYFIKOWANE",
                                                     "niepotwierdzona", "brak potwierdzonej afiliacji",
                                                     "komitet niepotwierdzony", "histor"):
                _, slug = normalise_party(p.party_note)
                if slug not in party_set and not any(s.isdigit() for s in slug):
                    party_name, _ = normalise_party(p.party_note)
                    party_set[slug] = Party(slug=slug, name=party_name)
                person_party_map.setdefault(p.name, []).append((p.party_note, slug))
    
    # Deduplicate party nodes: merge entries with same canonical name but different slugs
    # Keep the "best" slug (shortest and most canonical) for each canonical name
    best_slug_for_name = {}
    for slug, party in party_set.items():
        canonical = party.name.lower()
        if canonical not in best_slug_for_name or len(slug) < len(best_slug_for_name[canonical]):
            best_slug_for_name[canonical] = slug
    
    # Build a mapping: old_slug -> best_slug
    slug_mapping = {}
    for old_slug, party in party_set.items():
        canonical = party.name.lower()
        if canonical in best_slug_for_name:
            slug_mapping[old_slug] = best_slug_for_name[canonical]
    
    # Replace slugs in persons
    for uid_map in persons_map.values():
        for p in uid_map.values():
            new_slugs = []
            for slug in p.party_slugs:
                new_slug = slug_mapping.get(slug, slug)
                if new_slug not in new_slugs:
                    new_slugs.append(new_slug)
            # Also add slugs from party_note
            if p.party_note and p.party_note not in ("brak/niepotwierdzona", "brak danych", "NIEZWERYFIKOWANE",
                                                     "niepotwierdzona", "brak potwierdzonej afiliacji",
                                                     "komitet niepotwierdzony", "histor"):
                _, slug = normalise_party(p.party_note)
                new_slug = slug_mapping.get(slug, slug)
                if new_slug and new_slug not in new_slugs:
                    new_slugs.append(new_slug)
            p.party_slugs = new_slugs
    
    # Build final party list using only the "best" slugs
    final_parties = {}
    for slug, party in party_set.items():
        canonical = party.name.lower()
        if canonical in best_slug_for_name and best_slug_for_name[canonical] == slug:
            final_parties[slug] = party
    parties = list(final_parties.values())
    
    print(f"Collected {len(hospitals)} hospitals, {len(persons)} unique persons, {len(roles)} roles, {len(parties)} deduplicated parties")
    return hospitals, persons, roles, parties


# =========================================================================
# Main
# =========================================================================

def main():
    print("=" * 60)
    print("Hospital Graph Builder — Podlaskie Voivodeship")
    print("=" * 60)

    # Step 1: Build data
    print("\n[1/5] Building graph data from research...")
    hospitals, persons, roles, parties = build_data()

    # Step 2: Connect to Neo4j
    print("[2/5] Connecting to Neo4j...")
    driver = _connect()
    print(f"  ✓ Connected to {os.getenv('NEO4J_URI', 'bolt://localhost:7687')}")

    # Step 3: Load data
    print("[3/5] Loading data into Neo4j...")
    # Clear existing data
    with driver.session() as session:
        session.execute_write(_clear_database)
    load_hospitals(driver, hospitals)
    load_parties(driver, parties)
    load_persons(driver, persons)
    load_roles(driver, roles, {p.uid: p for p in persons})
    print("  ✓ Data loaded successfully")

    # Step 4: Validate
    print("[4/5] Validating graph...")
    validate(driver)

    # Export
    print("[5/5] Exporting reports...")
    export_overlap_csv(driver)
    print("  ↓ overlap_report.csv")

    # Create indexes (for performance)
    with driver.session() as session:
        session.execute_write(_create_indexes)

    driver.close()
    print("\nDone! Graph has been built in Neo4j.")
    print("Access Neo4j Browser at: http://localhost:7474")
    print("  Username: neo4j")
    print("  Password: graphdb123")


def _clear_database(tx):
    tx.run("MATCH (n) DETACH DELETE n")


def _create_indexes(tx):
    tx.run("CREATE INDEX hospital_uid IF NOT EXISTS FOR (h:Hospital) ON (h.uid)")
    tx.run("CREATE INDEX person_uid IF NOT EXISTS FOR (p:Person) ON (p.uid)")
    tx.run("CREATE INDEX person_normname IF NOT EXISTS FOR (p:Person) ON (p.normName)")


if __name__ == "__main__":
    main()
