-- ============================================
-- TEMPLATE-SAAS: Notifications Extended Tables
-- Schema: notifications
-- Version: 2.0.0
-- Fecha: 2026-01-07
-- Basado en: ET-SAAS-007-notifications-v2
-- Nota: Enums definidos en 02-enums.sql
-- ============================================

-- ============================================
-- TABLA: user_devices
-- Dispositivos registrados para push notifications
-- ============================================

CREATE TABLE IF NOT EXISTS notifications.user_devices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,

    -- Device info
    device_type notifications.device_type NOT NULL DEFAULT 'web',
    device_token TEXT NOT NULL,  -- PushSubscription JSON para Web Push
    device_name VARCHAR(100),    -- "Chrome en Windows", "Safari en macOS"

    -- Browser/OS info
    browser VARCHAR(50),
    browser_version VARCHAR(20),
    os VARCHAR(50),
    os_version VARCHAR(20),

    -- Status
    is_active BOOLEAN DEFAULT TRUE,

    -- Timestamps
    last_used_at TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,

    -- Constraints
    CONSTRAINT unique_user_device_token UNIQUE (user_id, device_token)
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_user_devices_user ON notifications.user_devices(user_id) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_user_devices_tenant ON notifications.user_devices(tenant_id);
CREATE INDEX IF NOT EXISTS idx_user_devices_active ON notifications.user_devices(is_active, last_used_at DESC);

-- RLS
ALTER TABLE notifications.user_devices ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS user_devices_tenant_isolation ON notifications.user_devices;
CREATE POLICY user_devices_tenant_isolation ON notifications.user_devices
    USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);

COMMENT ON TABLE notifications.user_devices IS 'Dispositivos registrados para push notifications (Web Push API)';
COMMENT ON COLUMN notifications.user_devices.device_token IS 'JSON serializado de PushSubscription del navegador';
COMMENT ON COLUMN notifications.user_devices.device_name IS 'Nombre descriptivo del dispositivo para UI';

-- ============================================
-- TABLA: notification_queue
-- Cola de procesamiento asincrono de notificaciones
-- ============================================

CREATE TABLE IF NOT EXISTS notifications.notification_queue (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    notification_id UUID NOT NULL REFERENCES notifications.notifications(id) ON DELETE CASCADE,

    -- Channel
    channel notifications.channel NOT NULL,

    -- Scheduling
    scheduled_for TIMESTAMPTZ DEFAULT NOW(),

    -- Priority (numerico para ordenamiento)
    priority_value INT DEFAULT 0,  -- urgent=10, high=5, normal=0, low=-5

    -- Processing
    attempts INT DEFAULT 0,
    max_attempts INT DEFAULT 3,
    status notifications.queue_status DEFAULT 'queued',

    -- Timestamps
    last_attempt_at TIMESTAMPTZ,
    next_retry_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,

    -- Error tracking
    error_message TEXT,
    error_count INT DEFAULT 0,

    -- Metadata
    metadata JSONB DEFAULT '{}'::jsonb,

    -- Audit
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

-- Indexes para procesamiento eficiente
CREATE INDEX IF NOT EXISTS idx_queue_pending ON notifications.notification_queue(scheduled_for, priority_value DESC)
    WHERE status IN ('queued', 'retrying');
CREATE INDEX IF NOT EXISTS idx_queue_status ON notifications.notification_queue(status);
CREATE INDEX IF NOT EXISTS idx_queue_notification ON notifications.notification_queue(notification_id);
CREATE INDEX IF NOT EXISTS idx_queue_retry ON notifications.notification_queue(next_retry_at)
    WHERE status = 'retrying' AND next_retry_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_queue_created ON notifications.notification_queue(created_at DESC);

COMMENT ON TABLE notifications.notification_queue IS 'Cola de procesamiento asincrono de notificaciones con BullMQ';
COMMENT ON COLUMN notifications.notification_queue.priority_value IS 'Valor numerico: urgent=10, high=5, normal=0, low=-5';
COMMENT ON COLUMN notifications.notification_queue.max_attempts IS 'Maximo de reintentos antes de marcar como failed';

-- ============================================
-- TABLA: notification_logs
-- Historial de entregas y eventos de notificaciones
-- ============================================

CREATE TABLE IF NOT EXISTS notifications.notification_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    notification_id UUID NOT NULL REFERENCES notifications.notifications(id) ON DELETE CASCADE,
    queue_id UUID REFERENCES notifications.notification_queue(id),

    -- Channel info
    channel notifications.channel NOT NULL,

    -- Delivery status
    status VARCHAR(30) NOT NULL,  -- 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained', 'failed'

    -- Provider info
    provider VARCHAR(50),  -- 'sendgrid', 'ses', 'web-push', 'smtp'
    provider_message_id VARCHAR(200),
    provider_response JSONB,

    -- Timestamps
    delivered_at TIMESTAMPTZ,
    opened_at TIMESTAMPTZ,
    clicked_at TIMESTAMPTZ,

    -- Error details
    error_code VARCHAR(50),
    error_message TEXT,

    -- Device info (for push)
    device_id UUID REFERENCES notifications.user_devices(id),

    -- Audit
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_logs_notification ON notifications.notification_logs(notification_id);
CREATE INDEX IF NOT EXISTS idx_logs_status ON notifications.notification_logs(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_logs_channel ON notifications.notification_logs(channel, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_logs_provider ON notifications.notification_logs(provider, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_logs_queue ON notifications.notification_logs(queue_id) WHERE queue_id IS NOT NULL;

COMMENT ON TABLE notifications.notification_logs IS 'Historial de entregas con tracking detallado por proveedor';
COMMENT ON COLUMN notifications.notification_logs.status IS 'sent, delivered, opened, clicked, bounced, complained, failed';
COMMENT ON COLUMN notifications.notification_logs.provider_response IS 'Respuesta cruda del proveedor para debugging';

-- ============================================
-- ACTUALIZAR user_preferences
-- Agregar campo para preferencias granulares
-- ============================================

-- Agregar columna para preferencias por tipo de notificacion
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.columns
        WHERE table_schema = 'notifications'
          AND table_name = 'user_preferences'
          AND column_name = 'notification_type_preferences'
    ) THEN
        ALTER TABLE notifications.user_preferences
        ADD COLUMN notification_type_preferences JSONB DEFAULT '{}'::jsonb;

        COMMENT ON COLUMN notifications.user_preferences.notification_type_preferences
        IS 'Preferencias por tipo: {"billing": {"email": true, "push": false}, "system": {"email": true}}';
    END IF;
END$$;

-- Agregar columna para frequencia de email digest
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.columns
        WHERE table_schema = 'notifications'
          AND table_name = 'user_preferences'
          AND column_name = 'email_frequency'
    ) THEN
        ALTER TABLE notifications.user_preferences
        ADD COLUMN email_frequency VARCHAR(20) DEFAULT 'immediate';

        COMMENT ON COLUMN notifications.user_preferences.email_frequency
        IS 'Frecuencia: immediate, daily, weekly';
    END IF;
END$$;

-- ============================================
-- FUNCIONES SQL
-- ============================================

-- Funcion: Calcular priority value desde string
CREATE OR REPLACE FUNCTION notifications.get_priority_value(p_priority VARCHAR)
RETURNS INT AS $$
BEGIN
    RETURN CASE p_priority
        WHEN 'urgent' THEN 10
        WHEN 'high' THEN 5
        WHEN 'normal' THEN 0
        WHEN 'low' THEN -5
        ELSE 0
    END;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

COMMENT ON FUNCTION notifications.get_priority_value IS 'Convierte priority string a valor numerico para ordenamiento';

-- Funcion: Encolar notificacion
CREATE OR REPLACE FUNCTION notifications.enqueue_notification(
    p_notification_id UUID,
    p_channel notifications.channel,
    p_priority VARCHAR DEFAULT 'normal',
    p_scheduled_for TIMESTAMPTZ DEFAULT NOW()
) RETURNS UUID AS $$
DECLARE
    v_queue_id UUID;
BEGIN
    INSERT INTO notifications.notification_queue (
        notification_id,
        channel,
        priority_value,
        scheduled_for,
        status
    ) VALUES (
        p_notification_id,
        p_channel,
        notifications.get_priority_value(p_priority),
        p_scheduled_for,
        'queued'
    ) RETURNING id INTO v_queue_id;

    RETURN v_queue_id;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.enqueue_notification IS 'Encola notificacion para procesamiento asincrono';

-- Funcion: Obtener items pendientes de la cola (con lock)
CREATE OR REPLACE FUNCTION notifications.get_pending_queue_items(
    p_limit INT DEFAULT 100,
    p_channel notifications.channel DEFAULT NULL
) RETURNS TABLE (
    id UUID,
    notification_id UUID,
    channel notifications.channel,
    priority_value INT,
    attempts INT,
    user_id UUID,
    tenant_id UUID,
    subject VARCHAR,
    body TEXT,
    body_html TEXT,
    recipient_email VARCHAR,
    metadata JSONB
) AS $$
BEGIN
    RETURN QUERY
    WITH pending AS (
        SELECT q.*
        FROM notifications.notification_queue q
        WHERE q.status IN ('queued', 'retrying')
          AND (q.scheduled_for IS NULL OR q.scheduled_for <= NOW())
          AND (q.next_retry_at IS NULL OR q.next_retry_at <= NOW())
          AND (p_channel IS NULL OR q.channel = p_channel)
        ORDER BY q.priority_value DESC, q.created_at ASC
        LIMIT p_limit
        FOR UPDATE SKIP LOCKED
    )
    SELECT
        p.id,
        p.notification_id,
        p.channel,
        p.priority_value,
        p.attempts,
        n.user_id,
        n.tenant_id,
        n.subject,
        n.body,
        n.body_html,
        n.recipient_email,
        n.metadata
    FROM pending p
    JOIN notifications.notifications n ON n.id = p.notification_id;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.get_pending_queue_items IS 'Obtiene items pendientes con FOR UPDATE SKIP LOCKED para procesamiento concurrente';

-- Funcion: Completar item de cola
CREATE OR REPLACE FUNCTION notifications.complete_queue_item(
    p_queue_id UUID,
    p_status VARCHAR,
    p_error_message TEXT DEFAULT NULL,
    p_provider VARCHAR DEFAULT NULL,
    p_provider_message_id VARCHAR DEFAULT NULL,
    p_provider_response JSONB DEFAULT NULL
) RETURNS VOID AS $$
DECLARE
    v_queue RECORD;
    v_notification_id UUID;
    v_channel notifications.channel;
BEGIN
    -- Obtener datos del queue item
    SELECT q.*, n.id as notif_id
    INTO v_queue
    FROM notifications.notification_queue q
    JOIN notifications.notifications n ON n.id = q.notification_id
    WHERE q.id = p_queue_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Queue item % not found', p_queue_id;
    END IF;

    v_notification_id := v_queue.notification_id;
    v_channel := v_queue.channel;

    IF p_status = 'sent' THEN
        -- Marcar como enviado
        UPDATE notifications.notification_queue
        SET status = 'sent',
            completed_at = NOW(),
            last_attempt_at = NOW(),
            attempts = attempts + 1
        WHERE id = p_queue_id;

        -- Actualizar notificacion
        UPDATE notifications.notifications
        SET status = 'sent',
            sent_at = NOW()
        WHERE id = v_notification_id
          AND status = 'pending';

        -- Crear log de exito
        INSERT INTO notifications.notification_logs (
            notification_id, queue_id, channel, status,
            provider, provider_message_id, provider_response,
            delivered_at
        ) VALUES (
            v_notification_id, p_queue_id, v_channel, 'sent',
            p_provider, p_provider_message_id, p_provider_response,
            NOW()
        );

    ELSIF p_status = 'failed' THEN
        IF v_queue.attempts + 1 >= v_queue.max_attempts THEN
            -- Fallo definitivo
            UPDATE notifications.notification_queue
            SET status = 'failed',
                last_attempt_at = NOW(),
                attempts = attempts + 1,
                error_message = p_error_message,
                error_count = error_count + 1
            WHERE id = p_queue_id;

            -- Actualizar notificacion
            UPDATE notifications.notifications
            SET status = 'failed',
                failed_at = NOW(),
                failure_reason = p_error_message
            WHERE id = v_notification_id;
        ELSE
            -- Programar retry con backoff exponencial (1, 2, 4, 8... minutos)
            UPDATE notifications.notification_queue
            SET status = 'retrying',
                last_attempt_at = NOW(),
                attempts = attempts + 1,
                next_retry_at = NOW() + (POWER(2, v_queue.attempts) * INTERVAL '1 minute'),
                error_message = p_error_message,
                error_count = error_count + 1
            WHERE id = p_queue_id;
        END IF;

        -- Crear log de error
        INSERT INTO notifications.notification_logs (
            notification_id, queue_id, channel, status,
            provider, error_message
        ) VALUES (
            v_notification_id, p_queue_id, v_channel, 'failed',
            p_provider, p_error_message
        );
    END IF;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.complete_queue_item IS 'Marca item como completado o fallido, con retry automatico';

-- Funcion: Limpiar notificaciones antiguas
CREATE OR REPLACE FUNCTION notifications.cleanup_old_notifications(
    p_days_to_keep INT DEFAULT 30
) RETURNS INT AS $$
DECLARE
    v_deleted INT;
BEGIN
    WITH deleted AS (
        DELETE FROM notifications.notifications
        WHERE created_at < NOW() - (p_days_to_keep || ' days')::INTERVAL
          AND is_read = TRUE
        RETURNING id
    )
    SELECT COUNT(*) INTO v_deleted FROM deleted;

    RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.cleanup_old_notifications IS 'Elimina notificaciones leidas mas antiguas que p_days_to_keep dias';

-- Funcion: Obtener dispositivos activos de un usuario
CREATE OR REPLACE FUNCTION notifications.get_user_active_devices(
    p_user_id UUID
) RETURNS TABLE (
    id UUID,
    device_type notifications.device_type,
    device_token TEXT,
    device_name VARCHAR,
    browser VARCHAR,
    os VARCHAR,
    last_used_at TIMESTAMPTZ
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        d.id,
        d.device_type,
        d.device_token,
        d.device_name,
        d.browser,
        d.os,
        d.last_used_at
    FROM notifications.user_devices d
    WHERE d.user_id = p_user_id
      AND d.is_active = TRUE
    ORDER BY d.last_used_at DESC;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.get_user_active_devices IS 'Obtiene dispositivos activos de un usuario para push notifications';

-- Funcion: Estadisticas de cola
CREATE OR REPLACE FUNCTION notifications.get_queue_stats()
RETURNS TABLE (
    status notifications.queue_status,
    channel notifications.channel,
    count BIGINT,
    oldest_created_at TIMESTAMPTZ
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        q.status,
        q.channel,
        COUNT(*)::BIGINT,
        MIN(q.created_at)
    FROM notifications.notification_queue q
    GROUP BY q.status, q.channel
    ORDER BY q.status, q.channel;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION notifications.get_queue_stats IS 'Estadisticas de la cola de notificaciones por status y canal';

-- ============================================
-- TRIGGERS
-- ============================================

-- Trigger para actualizar last_used_at en user_devices
CREATE OR REPLACE FUNCTION notifications.update_device_last_used()
RETURNS TRIGGER AS $$
BEGIN
    NEW.last_used_at := NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_user_devices_last_used ON notifications.user_devices;
CREATE TRIGGER trg_user_devices_last_used
    BEFORE UPDATE ON notifications.user_devices
    FOR EACH ROW
    WHEN (OLD.device_token IS DISTINCT FROM NEW.device_token)
    EXECUTE FUNCTION notifications.update_device_last_used();

-- ============================================
-- GRANTS (si se usa rol de aplicacion)
-- ============================================

-- Grants para el rol de aplicacion (descomentar si se usa)
-- GRANT SELECT, INSERT, UPDATE, DELETE ON notifications.user_devices TO app_user;
-- GRANT SELECT, INSERT, UPDATE, DELETE ON notifications.notification_queue TO app_user;
-- GRANT SELECT, INSERT ON notifications.notification_logs TO app_user;
-- GRANT EXECUTE ON FUNCTION notifications.enqueue_notification TO app_user;
-- GRANT EXECUTE ON FUNCTION notifications.get_pending_queue_items TO app_user;
-- GRANT EXECUTE ON FUNCTION notifications.complete_queue_item TO app_user;
-- GRANT EXECUTE ON FUNCTION notifications.get_user_active_devices TO app_user;

-- ============================================
-- FIN
-- ============================================
