"""registry_graph — corpus nacional de actos do registo e o grafo de participações.

Porque é que isto não vive nas tabelas que já existem:

* **`entity_relations` não consegue expressar propriedade.** Tem
  `CHECK (source_company_id < target_company_id)`, ou seja arestas não dirigidas,
  e é derivada de `process_parties` — partes de processos judiciais, das quais
  578 420 de 588 966 são *credores*. Duas empresas ficavam "relacionadas" porque
  a mesma pessoa reclamou créditos nas insolvências de ambas. Sai na 0025, depois
  de isto estar validado.
* **`publication_people` perde metade dos factos.** O índice
  `UNIQUE (publication_id, person_nif)` colapsa a dualidade sócio+gerente
  exactamente nas constituições, onde é a norma, e a quota ia concatenada no
  `cargo` como texto ("sócio — 125.000,00 Euros").
* **`dre_publications` é chaveada em `company_id`.** O corpus é nacional: ~159
  mil actos por ano, entidades que não monitorizamos nem queremos monitorizar.

E porque é que os nós **não** são linhas em `companies`: essa tabela é o universo
vigiado (205 linhas, com risk score, alvará e um scraper por NIF). Enchê-la com
centenas de milhares de entidades nacionais estragava todos os ecrãs e todos os
jobs que iteram sobre ela. Nada neste subsistema escreve em `companies`; a ligação
faz-se por `nif`, e há um endpoint para promover um nó a monitorizado quando
alguém decidir isso.

Dimensionamento medido no próprio CT: um dia nacional são 618 actos e 558
entidades, a listagem 3,9 s e o conteúdo 0,27 s por acto. Cinco anos ≈ 780 mil
actos, ~600 MB/ano com índices — contra 53 GB livres.
"""
from alembic import op

revision = "0023"
down_revision = "0022"
branch_labels = None
depends_on = None


def upgrade() -> None:
    # ------------------------------------------------------------------- nós
    op.execute(
        """
        CREATE TABLE registry_entities (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

            -- `nif` é NULL nas publicações de 2006 em prosa, onde há nome de
            -- titular e não há NIF. Um `nif UNIQUE` deixaria passar NULLs sem
            -- limite e cada acto criaria um nó novo para o mesmo titular, por
            -- isso a chave é `entity_key`: 'nif:510248900' ou 'name:<normalizado>'.
            nif CHAR(9),
            entity_key TEXT NOT NULL UNIQUE,
            nif_valid BOOLEAN NOT NULL DEFAULT FALSE,

            -- person/company/public/foreign pelo primeiro dígito do NIF;
            -- `unknown` (sem NIF) e `invalid` (mod-11 falha) existem como
            -- evidência mas **não propagam na travessia**: um dígito trocado
            -- num acto não pode fundir duas pessoas reais.
            kind TEXT NOT NULL DEFAULT 'unknown'
                CHECK (kind IN ('person','company','public','foreign','unknown','invalid')),

            name TEXT NOT NULL,
            name_norm TEXT NOT NULL,

            -- cabeçalho do registo, só faz sentido em nós que são entidades
            natureza_juridica TEXT,
            sede TEXT,
            distrito TEXT,
            concelho TEXT,
            freguesia TEXT,
            codigo_postal TEXT,
            localidade TEXT,
            pais TEXT,

            -- morada normalizada (rua+número+código postal). Não é para mostrar:
            -- é o que dá confiança à resolução de identidade de um cônjuge, que
            -- vem sempre sem NIF mas partilha a morada do titular.
            address_norm TEXT,

            -- estado societário derivado, recalculado e nunca escrito à mão
            captable_act_id UUID,
            captable_as_of DATE,
            captable_total NUMERIC(16,2),
            captable_currency CHAR(3),
            captable_complete BOOLEAN NOT NULL DEFAULT FALSE,

            -- materializado de propósito: a guarda anti-hub da travessia tem de
            -- ser um teste indexado, não uma subconsulta correlacionada. Um
            -- gerente nomeado em 400 empresas tornaria cada consulta a 2 hops
            -- num varrimento da tabela.
            degree_current INT NOT NULL DEFAULT 0,

            acts_count INT NOT NULL DEFAULT 0,
            last_act_date DATE,
            history_fetched_at TIMESTAMPTZ,

            first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
            updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
        )
        """
    )
    op.execute("CREATE UNIQUE INDEX ux_rent_nif ON registry_entities(nif) WHERE nif IS NOT NULL")
    op.execute("CREATE INDEX ix_rent_name_trgm ON registry_entities USING gin (name_norm gin_trgm_ops)")
    op.execute("CREATE INDEX ix_rent_kind_degree ON registry_entities(kind, degree_current DESC)")
    op.execute("CREATE INDEX ix_rent_addr ON registry_entities(address_norm) WHERE address_norm IS NOT NULL")
    op.execute("CREATE INDEX ix_rent_history ON registry_entities(history_fetched_at NULLS FIRST) WHERE kind IN ('company','public','foreign')")

    # ---------------------------------------------------------------- corpus
    op.execute(
        """
        CREATE TABLE registry_acts (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

            nipc CHAR(9) NOT NULL,
            irn_publication_id TEXT,
            entity_name TEXT,
            act_type TEXT,
            act_class TEXT NOT NULL DEFAULT 'outro',
            act_date DATE,
            apresentacao TEXT,
            district TEXT,
            council TEXT,
            listing_json JSONB,

            -- Texto integral, sem truncagem. O `dre_publications.summary` é
            -- cortado aos 4000 caracteres e é isso que impede reprocessar. Aqui
            -- o texto fica para o `parse_version` poder subir e as regex
            -- melhoradas serem reaplicadas sem voltar à rede: refazer um ano de
            -- conteúdo custa ~14 h de HTTP.
            body TEXT,
            body_sha256 CHAR(64),
            body_source TEXT NOT NULL DEFAULT 'irn'
                CHECK (body_source IN ('irn','dre_publications')),
            body_truncated BOOLEAN NOT NULL DEFAULT FALSE,

            content_status TEXT NOT NULL DEFAULT 'pending'
                CHECK (content_status IN ('pending','fetched','empty','error','skipped')),
            content_fetched_at TIMESTAMPTZ,
            content_attempts SMALLINT NOT NULL DEFAULT 0,
            wants_content BOOLEAN NOT NULL DEFAULT FALSE,
            priority SMALLINT NOT NULL DEFAULT 50,

            parse_version SMALLINT NOT NULL DEFAULT 0,
            parsed_at TIMESTAMPTZ,
            -- INT e não BOOLEAN: 0 e NULL são coisas diferentes. "parseei e não
            -- encontrei titulares" tem de se distinguir de "nunca parseei", e é
            -- essa distinção que permite detectar um parser morto — o defeito
            -- que passou meses sem ninguém dar por ele.
            parse_quotas INT,
            parse_people INT,
            parse_capital NUMERIC(16,2),
            parse_captable_complete BOOLEAN,
            parse_error TEXT,

            dedup_hash CHAR(64) NOT NULL UNIQUE,
            first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
            last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
        )
        """
    )
    op.execute("CREATE INDEX ix_racts_nipc_date ON registry_acts(nipc, act_date DESC NULLS LAST)")
    op.execute("CREATE INDEX ix_racts_date ON registry_acts(act_date DESC NULLS LAST)")
    # a fila de recolha
    op.execute(
        """
        CREATE INDEX ix_racts_queue ON registry_acts(priority, act_date DESC NULLS LAST)
         WHERE content_status = 'pending' AND wants_content
        """
    )
    # a fila de reparse
    op.execute("CREATE INDEX ix_racts_reparse ON registry_acts(parse_version) WHERE body IS NOT NULL")
    op.execute("CREATE INDEX ix_racts_class_date ON registry_acts(act_class, act_date DESC NULLS LAST)")
    # vigia de parser morto: actos que deviam ter cap table e não deram nenhuma
    op.execute(
        """
        CREATE INDEX ix_racts_zeroparse ON registry_acts(act_class)
         WHERE body IS NOT NULL AND parse_quotas = 0
        """
    )

    # ------------------------------------------------------------- arestas
    op.execute(
        """
        CREATE TABLE registry_edges (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

            -- Dirigidas: quem detém/gere aponta para a entidade do acto.
            -- "a xyz tem quotas na abc" e "a abc tem quotas na xyz" são duas
            -- linhas em sentidos opostos, ambas actuais — o ciclo que o
            -- `entity_relations` não conseguia sequer representar.
            holder_id UUID NOT NULL REFERENCES registry_entities(id) ON DELETE CASCADE,
            subject_id UUID NOT NULL REFERENCES registry_entities(id) ON DELETE CASCADE,

            edge_type TEXT NOT NULL CHECK (edge_type IN (
                'holds_quota','manages','audits','secretary','liquidator',
                'depositary','spouse_of','other')),
            role_label TEXT,

            quota_amount NUMERIC(16,2),
            currency CHAR(3),
            quota_share NUMERIC(7,4),

            valid_from DATE,
            valid_to DATE,
            is_current BOOLEAN NOT NULL DEFAULT TRUE,
            cease_cause TEXT,

            source_act_id UUID NOT NULL REFERENCES registry_acts(id) ON DELETE CASCADE,
            -- ordinal do titular dentro do bloco de quotas: a mesma pessoa pode
            -- deter duas quotas no mesmo acto, e isso não se soma na extracção
            snapshot_seq SMALLINT NOT NULL DEFAULT 0,
            confidence NUMERIC(3,2) NOT NULL DEFAULT 1.00,
            parse_version SMALLINT NOT NULL DEFAULT 0,
            created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

            CHECK (holder_id <> subject_id)
        )
        """
    )
    # `edge_type` na chave é o que mata o defeito do publication_people: a mesma
    # pessoa no mesmo acto como sócia e como gerente são dois factos distintos.
    op.execute(
        """
        CREATE UNIQUE INDEX ux_redges_assertion ON registry_edges(
            source_act_id, holder_id, subject_id, edge_type, snapshot_seq)
        """
    )
    op.execute("CREATE INDEX ix_redges_holder_cur ON registry_edges(holder_id, edge_type) WHERE is_current")
    op.execute("CREATE INDEX ix_redges_subject_cur ON registry_edges(subject_id, edge_type) WHERE is_current")
    op.execute("CREATE INDEX ix_redges_act ON registry_edges(source_act_id)")
    op.execute("CREATE INDEX ix_redges_subject_hist ON registry_edges(subject_id, edge_type, valid_from DESC NULLS LAST)")

    op.execute(
        """
        ALTER TABLE registry_entities
          ADD CONSTRAINT registry_entities_captable_act_fkey
          FOREIGN KEY (captable_act_id) REFERENCES registry_acts(id) ON DELETE SET NULL
        """
    )

    # -------------------------------------------------- resolução de identidade
    op.execute(
        """
        CREATE TABLE registry_identity_links (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

            -- o nó sem NIF (tipicamente um cônjuge) e o candidato com NIF
            unknown_id UUID NOT NULL REFERENCES registry_entities(id) ON DELETE CASCADE,
            candidate_id UUID NOT NULL REFERENCES registry_entities(id) ON DELETE CASCADE,

            -- 'nome+rua+cp' (alta) | 'nome+cp' (média) | 'nome' (baixa)
            evidence TEXT NOT NULL,
            score NUMERIC(3,2) NOT NULL,

            -- A travessia do grafo só atravessa ligações confirmadas por uma
            -- pessoa. Nome igual e morada igual é uma boa sugestão, não um
            -- facto: podem ter mudado de casa, e homónimos foi exactamente o
            -- que estragou as relações antigas.
            confirmed_at TIMESTAMPTZ,
            confirmed_by UUID REFERENCES users(id) ON DELETE SET NULL,
            rejected_at TIMESTAMPTZ,
            rejected_by UUID REFERENCES users(id) ON DELETE SET NULL,

            source_act_id UUID REFERENCES registry_acts(id) ON DELETE SET NULL,
            created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

            CHECK (unknown_id <> candidate_id)
        )
        """
    )
    op.execute("CREATE UNIQUE INDEX ux_ridlinks_pair ON registry_identity_links(unknown_id, candidate_id)")
    op.execute(
        """
        CREATE INDEX ix_ridlinks_pending ON registry_identity_links(score DESC)
         WHERE confirmed_at IS NULL AND rejected_at IS NULL
        """
    )
    op.execute("CREATE INDEX ix_ridlinks_confirmed ON registry_identity_links(unknown_id) WHERE confirmed_at IS NOT NULL")

    # -------------------------------------------------------- estado da recolha
    op.execute(
        """
        CREATE TABLE registry_sweep_days (
            day DATE PRIMARY KEY,
            status TEXT NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending','running','ok','partial','error')),
            listed_count INT,
            pages INT,
            new_count INT,
            attempts SMALLINT NOT NULL DEFAULT 0,
            last_error TEXT,
            started_at TIMESTAMPTZ,
            finished_at TIMESTAMPTZ
        )
        """
    )
    # Um registo explícito por dia, em vez do `MAX(params->>'date_to')` que o
    # resto do scheduler usa: esse padrão **não consegue representar um buraco**.
    # Uma corrida manual sobre uma janela posterior deixaria todos os dias
    # anteriores em falta órfãos para sempre — a 618 actos/dia não é detalhe.
    op.execute(
        """
        CREATE INDEX ix_rsweep_todo ON registry_sweep_days(day)
         WHERE status IN ('pending','error','partial')
        """
    )

    op.execute(
        """
        CREATE TABLE registry_entity_fetch (
            nif CHAR(9) PRIMARY KEY,
            depth SMALLINT NOT NULL DEFAULT 0,
            reason TEXT NOT NULL,
            seed_nif CHAR(9),
            status TEXT NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending','running','ok','error','skipped')),
            attempts SMALLINT NOT NULL DEFAULT 0,
            acts_seen INT,
            acts_new INT,
            last_error TEXT,
            history_fetched_at TIMESTAMPTZ,
            requested_at TIMESTAMPTZ NOT NULL DEFAULT now()
        )
        """
    )
    op.execute(
        """
        CREATE INDEX ix_rfetch_queue ON registry_entity_fetch(depth, requested_at)
         WHERE status IN ('pending','error')
        """
    )

    # ------------------------------------------------------------- incidentes
    op.execute(
        """
        CREATE TABLE entity_incident_summary (
            nif CHAR(9) PRIMARY KEY,
            cire_ann_count INT NOT NULL DEFAULT 0,
            cire_ann_as_debtor INT NOT NULL DEFAULT 0,
            cire_ann_last_date DATE,
            insolvency_open BOOLEAN NOT NULL DEFAULT FALSE,
            open_claim_deadline DATE,
            process_count INT NOT NULL DEFAULT 0,
            process_last_date DATE,
            contracts_count INT NOT NULL DEFAULT 0,
            risk_score INT,
            monitored_company_id UUID REFERENCES companies(id) ON DELETE SET NULL,
            monitoring_type TEXT,
            computed_at TIMESTAMPTZ NOT NULL DEFAULT now()
        )
        """
    )
    # Serve para pintar cada nó do grafo com os seus incidentes. Cruza por NIF
    # com o `cire_announcement_parties`, que é **nacional** (240 296 partes,
    # 55 321 NIFs) — portanto funciona para entidades e pessoas que não
    # monitorizamos, que é precisamente o que faltava.
    op.execute("CREATE INDEX ix_eis_severity ON entity_incident_summary(cire_ann_as_debtor DESC, cire_ann_count DESC)")
    op.execute("CREATE INDEX ix_eis_open ON entity_incident_summary(open_claim_deadline) WHERE insolvency_open")


def downgrade() -> None:
    op.execute("DROP TABLE IF EXISTS entity_incident_summary")
    op.execute("DROP TABLE IF EXISTS registry_entity_fetch")
    op.execute("DROP TABLE IF EXISTS registry_sweep_days")
    op.execute("DROP TABLE IF EXISTS registry_identity_links")
    op.execute("ALTER TABLE registry_entities DROP CONSTRAINT IF EXISTS registry_entities_captable_act_fkey")
    op.execute("DROP TABLE IF EXISTS registry_edges")
    op.execute("DROP TABLE IF EXISTS registry_acts")
    op.execute("DROP TABLE IF EXISTS registry_entities")
