-- +goose Up
-- records holds one durable notification record per accepted intent. The
-- (producer, idempotency_key) UNIQUE constraint replaces the previous Redis
-- idempotency keyspace: the durable row IS the idempotency reservation.
CREATE TABLE records (
    notification_id        text PRIMARY KEY,
    notification_type      text NOT NULL,
    producer               text NOT NULL,
    audience_kind          text NOT NULL,
    recipient_user_ids     jsonb NOT NULL DEFAULT '[]'::jsonb,
    payload_json           text NOT NULL,
    idempotency_key        text NOT NULL,
    request_fingerprint    text NOT NULL,
    request_id             text NOT NULL DEFAULT '',
    trace_id               text NOT NULL DEFAULT '',
    occurred_at            timestamptz NOT NULL,
    accepted_at            timestamptz NOT NULL,
    updated_at             timestamptz NOT NULL,
    idempotency_expires_at timestamptz NOT NULL,
    CONSTRAINT records_idempotency_unique UNIQUE (producer, idempotency_key)
);

-- Newest-first listing index used by operator/audit reads.
CREATE INDEX records_listing_idx
    ON records (accepted_at DESC, notification_id DESC);

-- routes stores one row per (notification_id, route_id). next_attempt_at is
-- non-NULL only while the row is a scheduling candidate (status pending or
-- failed); the partial index keeps the scheduler scan tight.
CREATE TABLE routes (
    notification_id           text NOT NULL
        REFERENCES records(notification_id) ON DELETE CASCADE,
    route_id                  text NOT NULL,
    channel                   text NOT NULL,
    recipient_ref             text NOT NULL,
    status                    text NOT NULL,
    attempt_count             integer NOT NULL DEFAULT 0,
    max_attempts              integer NOT NULL,
    next_attempt_at           timestamptz,
    resolved_email            text NOT NULL DEFAULT '',
    resolved_locale           text NOT NULL DEFAULT '',
    last_error_classification text NOT NULL DEFAULT '',
    last_error_message        text NOT NULL DEFAULT '',
    last_error_at             timestamptz,
    created_at                timestamptz NOT NULL,
    updated_at                timestamptz NOT NULL,
    published_at              timestamptz,
    dead_lettered_at          timestamptz,
    skipped_at                timestamptz,
    PRIMARY KEY (notification_id, route_id)
);

-- Drives the publishers' due-route pull. Partial predicate keeps the index
-- narrow: terminal rows (published / dead_letter / skipped) never appear.
CREATE INDEX routes_due_idx
    ON routes (next_attempt_at)
    WHERE next_attempt_at IS NOT NULL;

-- Coarse status / channel filters used by operator views.
CREATE INDEX routes_status_idx  ON routes (status);
CREATE INDEX routes_channel_idx ON routes (channel);

-- dead_letters carries the operator-visible record for one route that
-- exhausted automated handling. Cascade tied to the parent route row so a
-- record-level retention DELETE clears dependent dead-letter rows naturally.
CREATE TABLE dead_letters (
    notification_id        text NOT NULL,
    route_id               text NOT NULL,
    channel                text NOT NULL,
    recipient_ref          text NOT NULL,
    final_attempt_count    integer NOT NULL,
    max_attempts           integer NOT NULL,
    failure_classification text NOT NULL,
    failure_message        text NOT NULL,
    recovery_hint          text NOT NULL DEFAULT '',
    created_at             timestamptz NOT NULL,
    PRIMARY KEY (notification_id, route_id),
    FOREIGN KEY (notification_id, route_id)
        REFERENCES routes(notification_id, route_id) ON DELETE CASCADE
);

CREATE INDEX dead_letters_listing_idx
    ON dead_letters (created_at DESC, notification_id DESC, route_id DESC);

-- malformed_intents stores operator-visible records for stream entries the
-- intent validator could not accept. Independent retention pass.
CREATE TABLE malformed_intents (
    stream_entry_id   text PRIMARY KEY,
    notification_type text NOT NULL DEFAULT '',
    producer          text NOT NULL DEFAULT '',
    idempotency_key   text NOT NULL DEFAULT '',
    failure_code      text NOT NULL,
    failure_message   text NOT NULL,
    raw_fields        jsonb NOT NULL,
    recorded_at       timestamptz NOT NULL
);

CREATE INDEX malformed_intents_listing_idx
    ON malformed_intents (recorded_at DESC, stream_entry_id DESC);

-- +goose Down
DROP TABLE IF EXISTS malformed_intents;
DROP TABLE IF EXISTS dead_letters;
DROP TABLE IF EXISTS routes;
DROP TABLE IF EXISTS records;
