-- core: Discord-linked identity and callsign verification.

-- ---------------------------------------------------------------------------
-- users
-- ---------------------------------------------------------------------------

CREATE TABLE core.users (
    id                  bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

    -- Discord snowflakes are unsigned 64-bit, but the timestamp component keeps
    -- real values under 2^63 until roughly 2084, so bigint is safe and indexes
    -- far better than text.
    discord_id          bigint NOT NULL UNIQUE,

    -- NULL until the user claims one. Verification is a separate step: a claim
    -- is not a verification, and roles must key off verified_at, not this.
    callsign            core.callsign,
    callsign_verified_at timestamptz,
    verification_method text
        CHECK (verification_method IN ('arrl_email', 'qrz_profile', 'admin', 'lotw')),

    grid                core.gridsquare,
    timezone            text,           -- IANA name, e.g. 'Europe/Ljubljana'

    created_at          timestamptz NOT NULL DEFAULT now(),
    updated_at          timestamptz NOT NULL DEFAULT now(),

    -- A verified callsign must record how and when.
    CONSTRAINT users_verification_complete CHECK (
        (callsign_verified_at IS NULL AND verification_method IS NULL)
        OR (callsign_verified_at IS NOT NULL AND verification_method IS NOT NULL
            AND callsign IS NOT NULL)
    )
);

COMMENT ON TABLE core.users IS 'One row per Discord user who has interacted with a stateful command. Deliberately no soft-delete column: a deletion request must actually remove the row, so there is nothing left to resurrect.';

-- Two users may not both hold a VERIFIED claim on the same callsign. Unverified
-- claims are unconstrained, since anyone can type anything.
CREATE UNIQUE INDEX users_verified_callsign_uniq
    ON core.users (callsign)
    WHERE callsign_verified_at IS NOT NULL;

CREATE INDEX users_callsign_idx ON core.users (callsign) WHERE callsign IS NOT NULL;

CREATE TRIGGER users_touch
    BEFORE UPDATE ON core.users
    FOR EACH ROW EXECUTE FUNCTION core.touch_updated_at();

-- ---------------------------------------------------------------------------
-- verifications
-- ---------------------------------------------------------------------------

CREATE TABLE core.verifications (
    id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id         bigint NOT NULL REFERENCES core.users(id) ON DELETE CASCADE,

    method          text NOT NULL
        CHECK (method IN ('arrl_email', 'qrz_profile', 'admin', 'lotw')),
    callsign        core.callsign NOT NULL,

    status          text NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'succeeded', 'failed', 'expired', 'cancelled')),

    -- SHA-256 of the challenge token, never the token. Same reasoning as API
    -- keys: a database dump must not let anyone complete a verification.
    challenge_hash  bytea,

    -- Diagnostics only. Must NOT contain the email address or any other
    -- identifier - the whole point of hashing the challenge is undone if the
    -- address is sitting in a jsonb column next to it.
    detail          jsonb NOT NULL DEFAULT '{}'::jsonb,

    created_at      timestamptz NOT NULL DEFAULT now(),
    expires_at      timestamptz,
    completed_at    timestamptz
);

CREATE INDEX verifications_user_idx ON core.verifications (user_id, created_at DESC);

CREATE INDEX verifications_pending_idx
    ON core.verifications (expires_at)
    WHERE status = 'pending';

-- ---------------------------------------------------------------------------
-- subscriptions
-- ---------------------------------------------------------------------------

-- Discord-shaped, but persistent and worth backing up, so it belongs here
-- rather than in Redis.
CREATE TABLE core.subscriptions (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

    guild_id    bigint,                 -- NULL for a DM subscription
    channel_id  bigint NOT NULL,
    user_id     bigint REFERENCES core.users(id) ON DELETE CASCADE,

    kind        text NOT NULL
        CHECK (kind IN ('pota', 'sota', 'wwff', 'cluster', 'rbn', 'propagation', 'net')),

    -- Band, mode, region, distance, specific refs. Open-ended by nature, and
    -- the filter shape differs per kind, so jsonb rather than twenty columns
    -- that are NULL for most rows.
    filters     jsonb NOT NULL DEFAULT '{}'::jsonb,

    enabled     boolean NOT NULL DEFAULT true,
    created_at  timestamptz NOT NULL DEFAULT now(),
    updated_at  timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX subscriptions_channel_idx ON core.subscriptions (channel_id) WHERE enabled;
CREATE INDEX subscriptions_kind_idx ON core.subscriptions (kind) WHERE enabled;
CREATE INDEX subscriptions_filters_idx ON core.subscriptions USING gin (filters);

-- Postgres does not index the referencing side of a foreign key. Without this,
-- deleting a user sequentially scans subscriptions to enforce the cascade.
CREATE INDEX subscriptions_user_idx ON core.subscriptions (user_id) WHERE user_id IS NOT NULL;

CREATE TRIGGER subscriptions_touch
    BEFORE UPDATE ON core.subscriptions
    FOR EACH ROW EXECUTE FUNCTION core.touch_updated_at();
