#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
sdz · bloc Esdrúixoles #1 — «endevina quin text és real»
Consulta que genera resultat.txt. Executada el 2026-08-17 contra la base de
dades d'sdz (Supabase, API REST) amb les variables d'entorn SDZ_SUPABASE_URL i
SDZ_SUPABASE_SECRET.

    python3 consulta.py > resultat.txt

────────────────────────────────────────────────────────────────────────────
MÈTODE — llegeix això ABANS dels números
────────────────────────────────────────────────────────────────────────────

QUÈ VULL MESURAR, I PER QUÈ ÉS UNA PREGUNTA RARA
  L'article és un joc: fragments literals d'una convocatòria pública contra
  paròdies que he escrit jo. Per muntar-lo em calien fragments LITERALS, i
  abans d'anar a buscar-los a fora vaig fer el que semblava lògic: mirar si
  els tenia a la meva base de dades, que és on tinc desades les convocatòries.
  No hi eren. Aquesta consulta serveix per poder dir exactament QUANT no hi
  eren, i és una mesura del meu propi registre, no del sector.

  La pregunta, doncs: de tot el que arreplego de cada convocatòria, quanta
  PROSA seva en queda? És a dir, quantes paraules del text original —el que
  el joc necessita— sobreviuen al pas per la meva canonada?

D'ON SURTEN LES DADES
  · Taula `calls` — 1 fila = 1 convocatòria recollida (amb duplicats a dins;
    vegeu més avall). Columnes que existeixen, tal com les crea
    `db/schema.sql` i les migracions posteriors: id, source_id, legacy_id,
    organizer, title, call_type, disciplines, deadline, eligibility,
    geography, entry_fee, award, award_min, url, status, origin,
    poster_account, raw, created_at, updated_at.
  · Taula `call_requirements` — 1 fila = els requisits d'una convocatòria,
    extrets per la canonada «SDZ · resolve».

QUÈ COMPTO COM A «PROSA DE LA CONVOCATÒRIA»
  Els únics camps de text lliure i llarg que hi ha: `eligibility` (columna de
  `calls`) i `raw.notes` (dins del jsonb genèric). La resta són valors curts i
  tipats —dates, imports, tipus de convocatòria, disciplines, URL— o text
  lliure d'una sola dada (`entry_fee`, `award`).

  ⚠️ I EL MATÍS QUE ES MENJA LA MEITAT DEL RESULTAT: cap dels dos camps no
  està pensat per ser literal. Tots dos els omple un model extractor a partir
  de la pàgina o de la captura de pantalla —el prompt d'extracció és a
  `n8n/build_workflows.py` (EXTRACT_PROMPT, i el SCHEMA del crawl), i tots dos
  demanen extreure DADES, no transcriure—, o sigui que el que hi queda és el
  que el meu extractor va ANOTAR. Que algun valor coincideixi paraula per
  paraula amb l'original és possible i no ho he auditat fila a fila; el que sé
  és que res no ho garanteix i que res no marca quins ho serien. Per a un joc
  que necessita citacions literals, un camp sense aquesta garantia no serveix.
  Ho dic abans dels números i no després.

QUÈ NO PROVA AIXÒ
  ⛔ No prova res sobre com escriuen les convocatòries. No he mesurat el seu
     text: he mesurat què en desa el meu registre.
  ⛔ No prova que la prosa original no es pugui recuperar: les pàgines són a
     internet i `calls.url` sovint hi porta. Prova que jo no la tinc.
  ⛔ No és una mostra aleatòria de res. És el cens sencer de la meva taula el
     dia que s'ha executat, amb els seus duplicats i els seus buits.

EL RECOMPTE, I COM ES DEMANA
  Els totals es demanen amb `Prefer: count=exact` i es llegeixen del
  `Content-Range` de la resposta: PostgREST talla a 1.000 files i qualsevol
  recompte fet amb len() sobre la pàgina seria fals per sobre de mil.
  Les llargades sí que demanen baixar-se les files senceres, i es baixen
  paginades de mil en mil.
"""

import os
import statistics
from collections import Counter

import requests

URL = os.environ["SDZ_SUPABASE_URL"].rstrip("/")
KEY = os.environ["SDZ_SUPABASE_SECRET"]
H = {"apikey": KEY, "Authorization": f"Bearer {KEY}"}


def total(path: str) -> int:
    """Recompte exacte llegit del Content-Range, mai amb len()."""
    r = requests.get(
        f"{URL}/rest/v1/{path}",
        headers={**H, "Prefer": "count=exact", "Range": "0-0"},
        timeout=120,
    )
    r.raise_for_status()
    return int(r.headers["Content-Range"].split("/")[1])


def fetch_all(path: str, select: str, step: int = 1000) -> list:
    """Totes les files, paginades: PostgREST no en torna més de mil de cop."""
    out, offset = [], 0
    while True:
        r = requests.get(
            f"{URL}/rest/v1/{path}?select={select}&limit={step}&offset={offset}",
            headers=H,
            timeout=180,
        )
        r.raise_for_status()
        page = r.json()
        out += page
        if len(page) < step:
            return out
        offset += step


def resum(nom: str, valors: list) -> None:
    if not valors:
        print(f"  {nom}: cap valor")
        return
    print(
        f"  {nom}: n={len(valors)}  mediana={statistics.median(valors):.1f} car."
        f"  mitjana={statistics.mean(valors):.1f}  màx={max(valors)}"
    )


print("sdz · Esdrúixoles #1 — què desa el meu registre de cada convocatòria")
print("Executat el 2026-08-17 (hora d'aquí) contra la base de dades d'sdz.")
print("=" * 76)

n_calls = total("calls?select=id")
n_reqs = total("call_requirements?select=call_id")
print(f"\nFILES A `calls` (count=exact): {n_calls}")
print(f"FILES A `call_requirements` (count=exact): {n_reqs}")
print("⚠️  Files, no convocatòries: hi ha duplicats i la desduplicació està a")
print("    mig fer. Vegeu el recompte de `raw.duplicate_of` més avall.")

calls = fetch_all("calls", "id,title,eligibility,raw")
print(f"\nFiles baixades senceres per mesurar-ne les llargades: {len(calls)}")

# ── Les claus del jsonb genèric: què hi ha, de debò, dins de `raw` ──────────
claus = Counter()
for c in calls:
    if isinstance(c.get("raw"), dict):
        claus.update(c["raw"].keys())
print("\nCLAUS DE `raw` (jsonb genèric) i en quantes files surten:")
for k, v in claus.most_common():
    print(f"  {k}: {v}")

# ── Les llargades del text lliure ───────────────────────────────────────────
print("\nTEXT LLIURE CONSERVAT, en caràcters:")
elig = [len(c["eligibility"]) for c in calls if c.get("eligibility")]
notes = [
    len(c["raw"]["notes"])
    for c in calls
    if isinstance(c.get("raw"), dict) and isinstance(c["raw"].get("notes"), str)
]
titols = [len(c["title"]) for c in calls if c.get("title")]
resum("title       ", titols)
resum("eligibility ", elig)
resum("raw.notes   ", notes)
print(f"  files SENSE `eligibility`: {len(calls) - len(elig)}")
print("  ⚠️  Els tres camps els escriu el meu extractor, que té l'encàrrec")
print("      d'extreure dades, no de transcriure. Cap no ve garantit literal.")

# ── El requisit més llarg que he desat mai ─────────────────────────────────
reqs = fetch_all("call_requirements", "call_id,status,requirements")
items = []
for r in reqs:
    for it in r.get("requirements") or []:
        txt = " ".join(
            str(it.get(k) or "") for k in ("item", "details")
        ).strip()
        if txt:
            items.append(len(txt))
print("\nELS REQUISITS EXTRETS (`call_requirements.requirements`):")
print(f"  files: {len(reqs)}  ·  ítems en total: {len(items)}")
resum("llargada d'un ítem", items)
estats = Counter(r.get("status") for r in reqs)
print("  per estat: " + ", ".join(f"{k}={v}" for k, v in estats.most_common()))

# ── El text més llarg de tot el registre, sigui d'on sigui ─────────────────
tots = elig + notes + items
print("\nEL TEXT MÉS LLARG QUE HI HA A TOT EL REGISTRE:")
print(f"  {max(tots)} caràcters, d'un total de {len(tots)} trossos de text lliure.")
print(f"  Suma de tot el text lliure desat: {sum(tots)} caràcters")
print(f"  ≈ {sum(tots) / 5500:.1f} pàgines de 5.500 caràcters, per a {n_calls} files.")
print(f"  Mitjana per fila de `calls`: {sum(tots) / n_calls:.0f} caràcters.")
print("  (Per comparar: els tres fragments reals del quiz d'aquest article fan")
print("   268, 156 i 279 caràcters. El problema no és que no hi cabessin.)")

# ── La desduplicació, que segueix a mig fer ────────────────────────────────
dups = sum(
    1
    for c in calls
    if isinstance(c.get("raw"), dict) and c["raw"].get("duplicate_of")
)
print(f"\nFiles amb `raw.duplicate_of` marcat: {dups}")

print("\n" + "=" * 76)
print("Conclusió, dita amb el marge just: aquest registre no desa citacions.")
print("Desa dades tipades i, com a text lliure, anotacions del meu extractor —")
print("res que cap camp identifiqui com a paraules de la convocatòria. Per això")
print("els tres fragments reals del quiz d'aquest article es van llegir a mà de")
print("la pàgina, el 2026-08-11, i no d'aquí. Aquesta consulta és la")
print("comprovació de per què va caldre.")
