-- =============================================================================
-- Migration: 20260420000002_audit_logs_lgpd
-- Sessao 11 - Fase 2a (Opcao C).
--
-- Resolve: LGPD Art. 37 - registro das operacoes de tratamento.
-- Projeto ja tinha logs pontuais (document_access_logs, patient_status_history,
-- notification_logs, addon_transactions) mas nao registrava:
--   - Edicao de dados do paciente (nome/CPF/endereco)
--   - CRUD de sessoes na agenda
--   - CRUD de registros financeiros
--   - CRUD de documentos (metadata)
--   - Mudancas de permissao / members do tenant
--
-- Cria tabela audit_logs imutavel + funcao trigger generica + triggers nas
-- tabelas criticas. RLS: tenant member le; ninguem INSERT/UPDATE/DELETE direto.
-- =============================================================================

-- ---------------------------------------------------------------------------
-- Tabela audit_logs
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.audit_logs (
    id BIGSERIAL PRIMARY KEY,
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
    entity_type TEXT NOT NULL,
    entity_id TEXT,
    action TEXT NOT NULL CHECK (action IN ('insert', 'update', 'delete')),
    old_values JSONB,
    new_values JSONB,
    changed_fields TEXT[],
    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created
    ON public.audit_logs (tenant_id, created_at DESC);

CREATE INDEX IF NOT EXISTS idx_audit_logs_entity
    ON public.audit_logs (entity_type, entity_id);

CREATE INDEX IF NOT EXISTS idx_audit_logs_user_created
    ON public.audit_logs (user_id, created_at DESC) WHERE user_id IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_audit_logs_changed_fields
    ON public.audit_logs USING GIN (changed_fields);

COMMENT ON TABLE public.audit_logs IS
    'Registro imutavel de operacoes de tratamento (LGPD Art. 37). INSERT apenas via trigger SECURITY DEFINER.';

-- ---------------------------------------------------------------------------
-- Funcao trigger generica
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.log_audit_change()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
    v_tenant_id UUID;
    v_entity_id TEXT;
    v_old JSONB;
    v_new JSONB;
    v_changed TEXT[];
    v_heavy_fields TEXT[] := ARRAY[
        'content', 'content_html', 'content_json', 'raw_data',
        'signature_data', 'pdf_blob', 'binary', 'body_html', 'body_text'
    ];
    v_noise_fields TEXT[] := ARRAY['updated_at', 'last_seen_at', 'last_activity_at'];
BEGIN
    IF TG_OP = 'DELETE' THEN
        v_tenant_id := OLD.tenant_id;
        v_entity_id := OLD.id::TEXT;
        v_old := to_jsonb(OLD) - v_heavy_fields;
        v_new := NULL;
    ELSIF TG_OP = 'INSERT' THEN
        v_tenant_id := NEW.tenant_id;
        v_entity_id := NEW.id::TEXT;
        v_old := NULL;
        v_new := to_jsonb(NEW) - v_heavy_fields;
    ELSE -- UPDATE
        v_tenant_id := NEW.tenant_id;
        v_entity_id := NEW.id::TEXT;
        v_old := to_jsonb(OLD) - v_heavy_fields;
        v_new := to_jsonb(NEW) - v_heavy_fields;

        -- calcular campos realmente alterados
        SELECT array_agg(key ORDER BY key) INTO v_changed
        FROM jsonb_each(to_jsonb(NEW)) AS kv(key, value)
        WHERE (to_jsonb(OLD))->kv.key IS DISTINCT FROM kv.value;

        -- se nada mudou, ignora
        IF v_changed IS NULL THEN
            RETURN NEW;
        END IF;

        -- se mudou apenas campos de ruido (ex: updated_at), ignora
        IF v_changed <@ v_noise_fields THEN
            RETURN NEW;
        END IF;
    END IF;

    INSERT INTO public.audit_logs (
        tenant_id, user_id, entity_type, entity_id, action,
        old_values, new_values, changed_fields
    ) VALUES (
        v_tenant_id,
        auth.uid(),
        TG_TABLE_NAME,
        v_entity_id,
        lower(TG_OP),
        v_old,
        v_new,
        v_changed
    );

    RETURN COALESCE(NEW, OLD);
END;
$$;

COMMENT ON FUNCTION public.log_audit_change() IS
    'Trigger generica de audit. Filtra campos pesados (content, signature_data) e ruido (updated_at).';

-- ---------------------------------------------------------------------------
-- Triggers nas tabelas criticas
-- ---------------------------------------------------------------------------

-- patients
DROP TRIGGER IF EXISTS trg_audit_patients ON public.patients;
CREATE TRIGGER trg_audit_patients
    AFTER INSERT OR UPDATE OR DELETE ON public.patients
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_change();

-- agenda_eventos
DROP TRIGGER IF EXISTS trg_audit_agenda_eventos ON public.agenda_eventos;
CREATE TRIGGER trg_audit_agenda_eventos
    AFTER INSERT OR UPDATE OR DELETE ON public.agenda_eventos
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_change();

-- financial_records
DROP TRIGGER IF EXISTS trg_audit_financial_records ON public.financial_records;
CREATE TRIGGER trg_audit_financial_records
    AFTER INSERT OR UPDATE OR DELETE ON public.financial_records
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_change();

-- documents
DROP TRIGGER IF EXISTS trg_audit_documents ON public.documents;
CREATE TRIGGER trg_audit_documents
    AFTER INSERT OR UPDATE OR DELETE ON public.documents
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_change();

-- tenant_members (mudanca de permissao)
DROP TRIGGER IF EXISTS trg_audit_tenant_members ON public.tenant_members;
CREATE TRIGGER trg_audit_tenant_members
    AFTER INSERT OR UPDATE OR DELETE ON public.tenant_members
    FOR EACH ROW EXECUTE FUNCTION public.log_audit_change();

-- ---------------------------------------------------------------------------
-- RLS: tenant member le; saas_admin le tudo; ninguem escreve direto
-- ---------------------------------------------------------------------------
ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.audit_logs FORCE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS "audit_logs: select tenant" ON public.audit_logs;
DROP POLICY IF EXISTS "audit_logs: saas_admin all" ON public.audit_logs;
DROP POLICY IF EXISTS "audit_logs: no direct insert" ON public.audit_logs;
DROP POLICY IF EXISTS "audit_logs: no direct update" ON public.audit_logs;
DROP POLICY IF EXISTS "audit_logs: no direct delete" ON public.audit_logs;

CREATE POLICY "audit_logs: select tenant" ON public.audit_logs
    FOR SELECT TO authenticated
    USING (
        public.is_saas_admin()
        OR tenant_id IN (
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = auth.uid() AND tm.status = 'active'
        )
    );

-- Explicitamente NEGA insert/update/delete via API
-- (SECURITY DEFINER na funcao trigger bypassa RLS; app nao consegue escrever direto)
CREATE POLICY "audit_logs: no direct insert" ON public.audit_logs
    FOR INSERT TO authenticated
    WITH CHECK (false);

CREATE POLICY "audit_logs: no direct update" ON public.audit_logs
    FOR UPDATE TO authenticated
    USING (false) WITH CHECK (false);

CREATE POLICY "audit_logs: no direct delete" ON public.audit_logs
    FOR DELETE TO authenticated
    USING (false);

-- ---------------------------------------------------------------------------
-- Marca hardening na auditoria
-- ---------------------------------------------------------------------------
COMMENT ON COLUMN public.audit_logs.old_values IS 'Estado anterior (jsonb); NULL em INSERT; campos pesados removidos';
COMMENT ON COLUMN public.audit_logs.new_values IS 'Estado posterior (jsonb); NULL em DELETE; campos pesados removidos';
COMMENT ON COLUMN public.audit_logs.changed_fields IS 'Lista de campos alterados em UPDATE (NULL em INSERT/DELETE)';
