-- ============================================================
-- Migration 014: Row-Based Entry Lock Table
-- Date: 2026-02-18
-- Purpose: Replace session-based advisory locks with durable row locks.
-- ============================================================

CREATE TABLE IF NOT EXISTS entry_locks (
    profile_id uuid NOT NULL,
    symbol text NOT NULL,
    symbol_normalized text GENERATED ALWAYS AS (lower(symbol)) STORED NOT NULL,
    owner text NOT NULL,
    acquired_at timestamptz NOT NULL DEFAULT now(),
    expires_at timestamptz NOT NULL,
    PRIMARY KEY (profile_id, symbol_normalized)
);

CREATE INDEX IF NOT EXISTS entry_locks_expires_idx ON entry_locks (expires_at);

ALTER TABLE entry_locks ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS "Allow entry lock owner" ON entry_locks;
CREATE POLICY "Allow entry lock owner" ON entry_locks
    FOR ALL
    USING (auth.uid() = profile_id)
    WITH CHECK (auth.uid() = profile_id);

DROP POLICY IF EXISTS "Allow service role entry lock access" ON entry_locks;
CREATE POLICY "Allow service role entry lock access" ON entry_locks
    FOR ALL
    USING (auth.role() = 'service_role')
    WITH CHECK (auth.role() = 'service_role');

CREATE OR REPLACE FUNCTION fn_try_acquire_entry_lock_row(
    p_profile_id uuid,
    p_symbol text,
    p_owner text,
    p_ttl_seconds integer DEFAULT 30
)
RETURNS boolean AS
$$
DECLARE
    now_ts timestamptz := now();
    ttl integer := GREATEST(COALESCE(p_ttl_seconds, 30), 1);
    expires timestamptz := now_ts + make_interval(secs := ttl);
BEGIN
    INSERT INTO entry_locks (profile_id, symbol, owner, acquired_at, expires_at)
    VALUES (p_profile_id, p_symbol, p_owner, now_ts, expires)
    ON CONFLICT (profile_id, symbol_normalized)
    WHERE entry_locks.expires_at <= now_ts
    DO UPDATE SET
        owner = EXCLUDED.owner,
        acquired_at = EXCLUDED.acquired_at,
        expires_at = EXCLUDED.expires_at;
    RETURN TRUE;
EXCEPTION WHEN unique_violation THEN
    RETURN FALSE;
END;
$$
LANGUAGE plpgsql VOLATILE;

CREATE OR REPLACE FUNCTION fn_release_entry_lock_row(
    p_profile_id uuid,
    p_symbol text,
    p_owner text
)
RETURNS boolean AS
$$
DECLARE
    deleted_count integer;
BEGIN
    DELETE FROM entry_locks
    WHERE profile_id = p_profile_id
      AND symbol_normalized = lower(p_symbol)
      AND owner = p_owner;
    GET DIAGNOSTICS deleted_count = ROW_COUNT;
    RETURN deleted_count > 0;
END;
$$
LANGUAGE plpgsql VOLATILE;
