#!/usr/bin/env python3
"""Build the coded manifesto dataset (manifest-coded.csv) and aggregations for the
2026 Swedish valmanifest corpus.

Applies coding-rubric.md v3 (the long-form extension of the v2 machinery addendum).
Corpus: valmanifest/text/*.txt, extracted with `pdftotext -layout` from the publisher
PDFs archived in valmanifest/. Provenance: valmanifest-sources.md.

Two stages, deliberately separated by how much they can be trusted:

  Stage 1 (mechanical, reproducible by anyone running this file): a fixed lexicon is
  matched against each document; hits are segmented into passages; per-party counts
  are normalised per 1 000 words. Nothing here is a judgement call.

  Stage 2 (interpretive, single-coder): each passage carries a hand-assigned `frame`,
  `as_layer` and `own_claim`. These were assigned by reading the passages this script
  prints. They have NOT been double-coded. See the caveat in coding-rubric.md v3.
"""
import csv, re, glob, os, collections

# ---------------------------------------------------------------- stage 1: lexicon
# The lexicon is CORE + INFRA. It deliberately excludes high-frequency ambiguous words —
# `data` (everyday sense), `digital` (adjective, mostly "digitala tjänster"),
# `teknik`, `plattform` (collides with *valplattform*), `IT`, `upphandling`.
# Those are surveyed separately in coding-rubric.md v3 and are not part of the
# headline count, because a presence test is only as good as its false-positive rate.
CORE = (r"(artificiell intelligens|maskininlärning|\bAI\b|AI-\w*|AI:s|algoritm\w*|"
        r"språkmodell\w*|datacenter\w*|datacentral\w*|beräkningskraft\w*|"
        r"beräkningskapacitet\w*|superdator\w*|molntjänst\w*|öppen källkod|"
        r"öppna vikter|automatiser\w*|digitaliser\w*)")

# INFRA — added after an audit of the terms CORE excludes (see coding-rubric.md v3,
# "What the first lexicon missed"). CORE alone produced a FALSE NEGATIVE on the
# corpus's strongest sovereignty passage, M-04, because Moderaterna write "moln",
# not "molntjänst". Infrastructure- and dependency-language is therefore matched too.
INFRA = (r"(\bmoln\b|molnet|halvledare|beroende\w* av utländsk|utländsk teknik|"
         r"säkerhetskänslig\w*|kritiska system|digital infrastruktur|datadelning|"
         r"cyberskydd\w*|cybersäkerhet\w*|cyberangrepp\w*|cyberhot\w*|"
         r"cyberförsvar\w*|cyberförmåga\w*)")

LEX = f"({CORE}|{INFRA})"

# Passage segmentation: consecutive hits closer than GAP characters are one passage.
# GAP=250 chosen because the passage count is stable there (45 at 90, 32 at 250,
# 31 at 400) — i.e. it sits on the plateau, not on a slope.
GAP = 250

PARTIES = ["S", "M", "SD", "C", "V", "KD", "L", "MP"]

def load():
    docs = {}
    for f in sorted(glob.glob("valmanifest/text/*.txt")):
        p = os.path.basename(f).split("-")[0].upper()
        docs[p] = re.sub(r"\s+", " ", open(f).read())
    return docs

def passages(text):
    hits = list(re.finditer(LEX, text, re.I))
    out = []
    for m in hits:
        if out and m.start() - out[-1][1] < GAP:
            out[-1][1] = m.end()
        else:
            out.append([m.start(), m.end()])
    return hits, out

# --------------------------------------------------- stage 2: hand-assigned codes
# frame — how the passage frames the machinery. Value set, grounded in the corpus:
#   competitiveness  growth, jobs, "leda", attracting investment or talent
#   capacity         building or hosting technical capacity (compute, centres, data)
#   dependency       reliance on foreign technology named as a vulnerability
#   labour           retraining, skills, education, adaptation of workers
#   admin-efficiency faster casework, less administration, agencies using AI
#   regulation       rule-making about AI, domestic or international
#   rights           integrity, data protection, right to understand a decision
#   risk-social      harm to children, deepfakes, disinformation, screens
#   risk-security    cyber, hybrid threats, defence
#   risk-energy      electricity or resource cost of the infrastructure
#   inclusion        digital exclusion of citizens
#   culture          copyright, media, public service
#   toc              table-of-contents artefact; excluded from analysis
#
# as_layer — the Agentic State's twelve layers, as in rubric v2. ';'-separated, or none.
# own_claim — 1 if the passage asserts PUBLIC OWNERSHIP OR CONTROL of models, weights,
#   compute or data infrastructure. This is the variable the whole exercise tests.
CODES = {
 # --- Centerpartiet: the only manifesto with a dedicated AI chapter (1.6) ---
 "C-01": ("capacity",        "9",     0, "transporter och digital infrastruktur knyter ihop landet"),
 "C-02": ("toc",             "none",  0, "TOC: '1.6 AI och ny teknik som skapar fler jobb'"),
 "C-03": ("competitiveness", "none",  0, "Sverige ska leda i AI, innovation och ny tillväxt"),
 "C-04": ("labour",          "none",  0, "arbetsmarknaden förändras genom AI, digitalisering"),
 "C-05": ("competitiveness", "2",     0, "bli bäst på tillämpning av AI i privat och offentlig sektor"),
 "C-06": ("labour",          "3",     0, "Lägg om jobb- och utbildningspolitiken för AI-eran"),
 "C-07": ("capacity",        "9;8",   0, "nationellt AI-center som samlar forskning, beräkningskraft och näringsliv"),
 "C-08": ("rights",          "7;8",   0, "människors rätt att förstå beslut som påverkar deras liv"),
 "C-09": ("labour",          "none",  0, "Ge alla AI-kunskap (elever)"),
 "C-10": ("inclusion",       "1",     0, "Ingen ska hamna utanför i digitaliseringen"),
 "C-11": ("admin-efficiency","2",     0, "digitaliseringen användas för att förenkla, inte skapa krångel"),
 "C-12": ("admin-efficiency","2;6",   0, "införa AI i stor skala ... snabbspår för AI-upphandling"),
 "C-13": ("capacity",        "9",     0, "investeringar i vägar, järnväg och digital infrastruktur"),
 "C-14": ("risk-security",   "5;10",  0, "digital infrastruktur ska fungera även i kris"),
 "C-15": ("regulation",      "3",     0, "aktiv röst om AI i EU för gemensamma regler"),
 "C-16": ("risk-security",   "5;10",  0, "kunna betala även vid elavbrott, nätstörningar och cyberangrepp"),
 # --- Kristdemokraterna: one passage in the whole manifesto ---
 "KD-01": ("risk-social",    "none",  0, "Skydda barn från techjättarnas skadliga algoritmer"),
 # --- Liberalerna ---
 "L-01": ("risk-social",     "none",  0, "när AI är vardag blir källkritik viktigare"),
 "L-02": ("risk-social",     "none",  0, "Digitaliseringen i skolan har gått för långt"),
 "L-03": ("risk-social",     "none",  0, "stoppa kränkande AI-deepfakes"),
 "L-04": ("dependency",      "9;10",  0, "beroende av digital teknik från andra delar av världen gör oss sårbara; Europa ska stärka egen kapacitet inom halvledare, molntjänster, AI"),
 "L-05": ("competitiveness", "none",  0, "Sverige ska leda teknikrevolutionen"),
 "L-06": ("labour",          "12",    0, "AI-kunskapslyft"),
 "L-07": ("risk-social",     "none",  0, "när algoritmer styr nyhetsflödet ökar polariseringen"),
 # --- Moderaterna ---
 "M-01": ("risk-social",     "none",  0, "rätt att stoppa AI-genererade nakenbilder"),
 "M-02": ("risk-security",   "10",    0, "byggt upp en starkare svensk cyberförmåga"),
 "M-03": ("risk-security",   "10",    0, "nationellt cyberskydd för Sveriges kommuner, övervakning dygnet runt"),
 "M-04": ("dependency",      "6;9;10",0, "minska beroenden av utländsk teknik i kritiska system genom strategisk upphandling för svensk och europeisk teknik inom säkerhetskänslig IT, moln och drift"),
 "M-05": ("culture",         "none",  0, "AI utmanar affärsmodeller, upphovsrätt och verifiering"),
 "M-06": ("risk-social",     "none",  0, "AI-kunskap och medie- och informationskunskap"),
 # --- Socialdemokraterna ---
 "S-01": ("competitiveness", "none",  0, "peka ut tekniker där Sverige ska vara ledande, som AI"),
 "S-02": ("competitiveness", "none",  0, "investera i bostäder, järnväg, el, digitalisering, forskning"),
 "S-03": ("admin-efficiency","2",     0, "digitalisering och AI för att korta handläggningstiderna"),
 "S-04": ("risk-energy",     "none",  0, "krav vid energiintensiva etableringar, exempelvis datacenter"),
 "S-05": ("labour",          "12",    0, "Ett svenskt AI program (kompetens, jfr Hem-pc)"),
 "S-06": ("labour",          "none",  0, "med ny teknik - inte minst AI - viktigare med omställning"),
 "S-07": ("labour",          "none",  0, "kursen i artificiell intelligens inställd (folkbildning)"),
 "S-08": ("admin-efficiency","2;4;8", 0, "AI-tekniken används för att samköra uppgifter"),
 "S-09": ("risk-security",   "10",    0, "informations- och cybersäkerhet i totalförsvaret"),
}

FRAMES = ["competitiveness","capacity","dependency","labour","admin-efficiency","regulation",
          "rights","risk-social","risk-security","risk-energy","inclusion","culture"]
LAYER_NAMES = {1:"Public Service Design/UX",2:"Government Workflows",3:"Policy- and Rule-Making",
               4:"Regulatory Compliance",5:"Crisis Response",6:"Public Procurement",
               7:"Agent Governance",8:"Data and Privacy",9:"Tech Stack",
               10:"Cyber Security/Resilience",11:"Public Finance",12:"People and Culture"}

# ============================================================== build
docs = load()
rows, doclevel = [], []
for p in PARTIES:
    t = docs.get(p, "")
    words = len(t.split())
    hits, pas = passages(t)
    doclevel.append({"party": p, "words": words, "hits": len(hits), "passages": len(pas),
                     "hits_per_1k": round(len(hits)/words*1000, 2) if words else 0.0})
    for i, (a, b) in enumerate(pas, 1):
        pid = f"{p}-{i:02d}"
        terms = ";".join(sorted({m.group(0).lower() for m in re.finditer(LEX, t[a:b+1], re.I)}))
        frame, layer, own, anchor = CODES[pid]
        rows.append({"id": pid, "party": p, "terms": terms, "frame": frame,
                     "as_layer": layer, "own_claim": own, "anchor": anchor,
                     "context": t[max(0, a-160):b+240].strip()})

missing = set(CODES) ^ {r["id"] for r in rows}
assert not missing, f"codes out of sync with extraction: {sorted(missing)}"

cols = ["id","party","terms","frame","as_layer","own_claim","anchor","context"]
with open("manifest-coded.csv","w",newline="") as f:
    w = csv.DictWriter(f, fieldnames=cols); w.writeheader()
    for r in rows: w.writerow(r)

# ============================================================== report
print("=" * 74)
print("VALMANIFEST 2026 — MACHINERY CODING (coding-rubric.md v3)")
print("=" * 74)

print("\nSTAGE 1 — mechanical. Core-lexicon hits, normalised per 1 000 words.\n")
print(f"  {'party':<7}{'words':>8}{'hits':>7}{'passages':>10}{'per 1k':>9}")
for d in sorted(doclevel, key=lambda x: -x["hits_per_1k"]):
    print(f"  {d['party']:<7}{d['words']:>8}{d['hits']:>7}{d['passages']:>10}{d['hits_per_1k']:>9.2f}")
tw = sum(d["words"] for d in doclevel); th = sum(d["hits"] for d in doclevel)
print(f"  {'TOTAL':<7}{tw:>8}{th:>7}{len(rows):>10}{th/tw*1000:>9.2f}")
zero = [d["party"] for d in doclevel if d["hits"] == 0]
print(f"\n  Manifestos with ZERO core-lexicon hits: {', '.join(zero) if zero else 'none'}")

print("\n  Absent from ALL eight manifestos (0 occurrences in the whole corpus):")
for term in ["språkmodell","öppen källkod","öppna vikter","leverantörsberoende",
             "digital suveränitet","folkbibliotek för modeller","beräkningscentrum"]:
    n = sum(len(re.findall(term, t, re.I)) for t in docs.values())
    if n == 0: print(f"    · {term}")

print("\nSTAGE 2 — interpretive, single-coder, NOT double-coded.\n")
print("  FRAME — how the machinery is framed when it does appear:\n")
fc = collections.Counter(r["frame"] for r in rows if r["frame"] != "toc")
n_coded = sum(fc.values())
for fr in FRAMES:
    if fc[fr]:
        bar = "#" * fc[fr]
        parties = ",".join(sorted({r["party"] for r in rows if r["frame"] == fr}))
        print(f"    {fr:<17}{fc[fr]:>3}  {bar:<12} {parties}")
print(f"    {'(toc, excluded)':<17}{sum(1 for r in rows if r['frame']=='toc'):>3}")

print("\n  LAYER — which of the Agentic State's twelve layers the passage touches:\n")
lc = collections.Counter()
for r in rows:
    if r["as_layer"] != "none":
        for L in r["as_layer"].split(";"): lc[int(L)] += 1
for L in range(1, 13):
    band = "implementation" if L <= 6 else "ENABLEMENT"
    print(f"    L{L:<3}{LAYER_NAMES[L]:<28}{lc[L]:>3}  {'#'*lc[L]:<8} {band}")
impl, enab = sum(lc[L] for L in range(1,7)), sum(lc[L] for L in range(7,13))
print(f"\n    implementation (1-6): {impl}   ENABLEMENT (7-12): {enab}"
      f"   no layer: {sum(1 for r in rows if r['as_layer']=='none')}")

print("\n  OWNERSHIP CLAIM — passages asserting public ownership or control of")
print("  models, weights, compute or data infrastructure:\n")
own = [r for r in rows if r["own_claim"] == 1]
print(f"    {len(own)} of {n_coded} coded passages, across {len(PARTIES)} manifestos"
      f" and {tw:,} words.")
if own:
    for r in own: print(f"      {r['id']} {r['party']}: {r['anchor']}")
else:
    print("    The nearest approaches, none of which makes the claim:")
    for pid in ("C-07", "M-04", "L-04"):
        r = next(x for x in rows if x["id"] == pid)
        print(f"      {r['id']} ({r['frame']}, L{r['as_layer']}): {r['anchor']}")
print()
