-- ──────────────────────────────────────────────────────────────────────────
-- Tracecoin security hardening
-- ──────────────────────────────────────────────────────────────────────────
-- This migration tightens the tracecoin_wallets + tracecoin_ledger schema
-- so that the wallet/ledger can be operated on safely under concurrent
-- load. Specifically:
--
--   1. Prevent negative balances at the database layer (CHECK constraints).
--   2. Prevent double-credits on payment re-verification (unique idempotency
--      key on (wallet_id, reference_id) when reference_id IS NOT NULL).
--   3. Add a balance_after column so the ledger is a self-contained
--      append-only history with running totals (no need to SUM at read time).
--   4. Add an actor_user_id column so we know who triggered each entry
--      (admin adjustments become auditable; system entries are NULL).
--   5. Add metadata JSONB so callers can attach context without growing
--      the schema.
--   6. Backfill balance_after for existing rows.
--   7. Add a CHECK that amount != 0.
--   8. Add a CHECK that type is one of the allowed values.
--   9. Add an index on (user_id, created_at DESC) for fast history reads.
-- ──────────────────────────────────────────────────────────────────────────

-- 1. CHECK constraints
ALTER TABLE tracecoin_wallets
    DROP CONSTRAINT IF EXISTS tracecoin_wallets_balance_nonneg;
ALTER TABLE tracecoin_wallets
    ADD CONSTRAINT tracecoin_wallets_balance_nonneg
    CHECK (balance >= 0);

ALTER TABLE tracecoin_wallets
    DROP CONSTRAINT IF EXISTS tracecoin_wallets_reserved_nonneg;
ALTER TABLE tracecoin_wallets
    ADD CONSTRAINT tracecoin_wallets_reserved_nonneg
    CHECK (reserved >= 0);

ALTER TABLE tracecoin_wallets
    DROP CONSTRAINT IF EXISTS tracecoin_wallets_balance_consistent;
ALTER TABLE tracecoin_wallets
    ADD CONSTRAINT tracecoin_wallets_balance_consistent
    CHECK (balance + reserved <= 2147483647);

-- 2. Idempotency: at most one ledger entry per (wallet, reference, type).
-- This lets a single payment credit exactly once, while still allowing
-- separate RESERVE / RELEASE / CONFIRM rows for a single lead_request.
CREATE UNIQUE INDEX IF NOT EXISTS uq_tracecoin_ledger_wallet_reference_type
    ON tracecoin_ledger (wallet_id, reference_id, type)
    WHERE reference_id IS NOT NULL;

-- 3. balance_after column
ALTER TABLE tracecoin_ledger
    ADD COLUMN IF NOT EXISTS balance_after INTEGER;

-- 4. actor_user_id column
ALTER TABLE tracecoin_ledger
    ADD COLUMN IF NOT EXISTS actor_user_id UUID REFERENCES users(id);

-- 5. metadata JSONB
ALTER TABLE tracecoin_ledger
    ADD COLUMN IF NOT EXISTS metadata JSONB;

-- 7. amount != 0
ALTER TABLE tracecoin_ledger
    DROP CONSTRAINT IF EXISTS tracecoin_ledger_amount_nonzero;
ALTER TABLE tracecoin_ledger
    ADD CONSTRAINT tracecoin_ledger_amount_nonzero
    CHECK (amount <> 0);

-- 8. type must be one of the allowed values
ALTER TABLE tracecoin_ledger
    DROP CONSTRAINT IF EXISTS tracecoin_ledger_type_valid;
ALTER TABLE tracecoin_ledger
    ADD CONSTRAINT tracecoin_ledger_type_valid
    CHECK (type IN ('CREDIT', 'DEBIT', 'RESERVE', 'RELEASE', 'ADJUSTMENT'));

-- 9. (user_id, created_at) covering index for history reads.
-- We don't have user_id on the ledger; we have wallet_id, so we use that.
CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_wallet_created
    ON tracecoin_ledger (wallet_id, created_at DESC);

CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_type
    ON tracecoin_ledger (type);

-- 6. Backfill balance_after for existing rows using a window function.
-- This is best-effort: if the data has gaps, balance_after is set to the
-- row's amount as a relative number; a re-compute can rebuild it later.
WITH ordered AS (
    SELECT
        id,
        SUM(amount) OVER (
            PARTITION BY wallet_id
            ORDER BY created_at, id
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS running
    FROM tracecoin_ledger
)
UPDATE tracecoin_ledger tl
SET balance_after = ordered.running
FROM ordered
WHERE tl.id = ordered.id
  AND tl.balance_after IS NULL;

-- Prevent further updates / deletes to ledger rows. The application code
-- must treat the ledger as append-only.
CREATE OR REPLACE FUNCTION tracecoin_ledger_immutable()
RETURNS TRIGGER AS $$
BEGIN
    RAISE EXCEPTION 'tracecoin_ledger is append-only; updates and deletes are not allowed';
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_tracecoin_ledger_no_update ON tracecoin_ledger;
CREATE TRIGGER trg_tracecoin_ledger_no_update
    BEFORE UPDATE ON tracecoin_ledger
    FOR EACH ROW EXECUTE FUNCTION tracecoin_ledger_immutable();

DROP TRIGGER IF EXISTS trg_tracecoin_ledger_no_delete ON tracecoin_ledger;
CREATE TRIGGER trg_tracecoin_ledger_no_delete
    BEFORE DELETE ON tracecoin_ledger
    FOR EACH ROW EXECUTE FUNCTION tracecoin_ledger_immutable();

-- Helper function: get-or-create wallet, lock the row, return id and current
-- balance. This is the single point where concurrent access is serialized.
CREATE OR REPLACE FUNCTION lock_tracecoin_wallet(p_user_id UUID)
RETURNS TABLE(wallet_id UUID, balance INTEGER, reserved INTEGER)
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- Insert if missing. ON CONFLICT DO NOTHING preserves an existing row.
    INSERT INTO tracecoin_wallets (user_id, balance, reserved)
    VALUES (p_user_id, 0, 0)
    ON CONFLICT (user_id) DO NOTHING;

    SELECT id INTO v_id FROM tracecoin_wallets WHERE user_id = p_user_id;

    -- Lock the row for the duration of the transaction.
    RETURN QUERY
        SELECT tw.id, tw.balance, tw.reserved
        FROM tracecoin_wallets tw
        WHERE tw.id = v_id
        FOR UPDATE;
END;
$$;
