-- =============================================================================
-- Migration: 20260419000010_documents_security_hardening
-- Sessão 6 — revisão sênior de Documentos. Resolve V#43-V#49 (5 críticos/altos
-- + 2 médios). V#50-V#52 (portal-paciente, hash, retention) ficam pendentes
-- pra próxima sessão (precisam de design/decisão).
--
-- Path convention dos buckets: "{tenant_id}/{patient_id}/{timestamp}-{file}"
--   (storage.foldername(name))[1] = tenant_id
-- =============================================================================

-- Tabelas de documents são owned por supabase_admin
SET LOCAL ROLE supabase_admin;

-- ─────────────────────────────────────────────────────────────────────────
-- V#43 + V#44: storage.objects para buckets "documents" e "generated-docs"
-- -----------------------------------------------------------------------------
DROP POLICY IF EXISTS "documents: authenticated read"   ON storage.objects;
DROP POLICY IF EXISTS "documents: authenticated upload" ON storage.objects;
DROP POLICY IF EXISTS "documents: authenticated delete" ON storage.objects;
DROP POLICY IF EXISTS "documents: tenant member read"   ON storage.objects;
DROP POLICY IF EXISTS "documents: tenant member upload" ON storage.objects;
DROP POLICY IF EXISTS "documents: tenant member delete" ON storage.objects;

CREATE POLICY "documents: tenant member read" ON storage.objects
  FOR SELECT TO authenticated
  USING (
    bucket_id = 'documents'
    AND (
        public.is_saas_admin()
        OR
        (storage.foldername(name))[1]::uuid IN (
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = auth.uid() AND tm.status = 'active'
        )
    )
  );

CREATE POLICY "documents: tenant member upload" ON storage.objects
  FOR INSERT TO authenticated
  WITH CHECK (
    bucket_id = 'documents'
    AND (storage.foldername(name))[1]::uuid IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
  );

CREATE POLICY "documents: tenant member delete" ON storage.objects
  FOR DELETE TO authenticated
  USING (
    bucket_id = 'documents'
    AND (
        public.is_saas_admin()
        OR
        (storage.foldername(name))[1]::uuid IN (
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = auth.uid() AND tm.status = 'active'
        )
    )
  );

DROP POLICY IF EXISTS "generated-docs: authenticated read"   ON storage.objects;
DROP POLICY IF EXISTS "generated-docs: authenticated upload" ON storage.objects;
DROP POLICY IF EXISTS "generated-docs: authenticated delete" ON storage.objects;
DROP POLICY IF EXISTS "generated-docs: tenant member read"   ON storage.objects;
DROP POLICY IF EXISTS "generated-docs: tenant member upload" ON storage.objects;
DROP POLICY IF EXISTS "generated-docs: tenant member delete" ON storage.objects;

CREATE POLICY "generated-docs: tenant member read" ON storage.objects
  FOR SELECT TO authenticated
  USING (
    bucket_id = 'generated-docs'
    AND (
        public.is_saas_admin()
        OR
        (storage.foldername(name))[1]::uuid IN (
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = auth.uid() AND tm.status = 'active'
        )
    )
  );

CREATE POLICY "generated-docs: tenant member upload" ON storage.objects
  FOR INSERT TO authenticated
  WITH CHECK (
    bucket_id = 'generated-docs'
    AND (storage.foldername(name))[1]::uuid IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
  );

CREATE POLICY "generated-docs: tenant member delete" ON storage.objects
  FOR DELETE TO authenticated
  USING (
    bucket_id = 'generated-docs'
    AND (
        public.is_saas_admin()
        OR
        (storage.foldername(name))[1]::uuid IN (
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = auth.uid() AND tm.status = 'active'
        )
    )
  );

-- ─────────────────────────────────────────────────────────────────────────
-- V#45: documents — policies separadas por cmd
-- -----------------------------------------------------------------------------
DROP POLICY IF EXISTS "documents: owner full access" ON public.documents;
DROP POLICY IF EXISTS "documents: select"             ON public.documents;
DROP POLICY IF EXISTS "documents: insert"             ON public.documents;
DROP POLICY IF EXISTS "documents: update"             ON public.documents;
DROP POLICY IF EXISTS "documents: delete"             ON public.documents;

-- SELECT: owner OR tenant_member ativo OR saas_admin
CREATE POLICY "documents: select" ON public.documents
  FOR SELECT TO authenticated
  USING (
    owner_id = auth.uid()
    OR 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'
    )
  );

-- INSERT: owner_id deve ser o caller, tenant_id deve ser tenant ativo do caller
CREATE POLICY "documents: insert" ON public.documents
  FOR INSERT TO authenticated
  WITH CHECK (
    owner_id = auth.uid()
    AND tenant_id IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
  );

-- UPDATE: só owner
CREATE POLICY "documents: update" ON public.documents
  FOR UPDATE TO authenticated
  USING (owner_id = auth.uid() OR public.is_saas_admin())
  WITH CHECK (owner_id = auth.uid() OR public.is_saas_admin());

-- DELETE: só owner ou saas_admin
CREATE POLICY "documents: delete" ON public.documents
  FOR DELETE TO authenticated
  USING (owner_id = auth.uid() OR public.is_saas_admin());

-- ─────────────────────────────────────────────────────────────────────────
-- V#46: document_share_links — RPC validate_share_token + remover SELECT direto
-- -----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.validate_share_token(p_token text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
DECLARE
    sl document_share_links%ROWTYPE;
    v_doc documents%ROWTYPE;
    v_token text;
BEGIN
    v_token := nullif(btrim(coalesce(p_token, '')), '');
    IF v_token IS NULL THEN
        RAISE EXCEPTION 'token obrigatório' USING ERRCODE = '22023';
    END IF;

    SELECT * INTO sl FROM document_share_links WHERE token = v_token LIMIT 1;
    IF NOT FOUND THEN
        RAISE EXCEPTION 'Token inválido' USING ERRCODE = '28000';
    END IF;
    IF sl.ativo IS NOT TRUE THEN
        RAISE EXCEPTION 'Link desativado' USING ERRCODE = '28000';
    END IF;
    IF sl.expira_em IS NOT NULL AND sl.expira_em < now() THEN
        RAISE EXCEPTION 'Link expirado' USING ERRCODE = '28000';
    END IF;
    IF sl.usos_max IS NOT NULL AND sl.usos >= sl.usos_max THEN
        RAISE EXCEPTION 'Limite de uso atingido' USING ERRCODE = '28000';
    END IF;

    -- Incrementa uso atomicamente
    UPDATE document_share_links SET usos = usos + 1 WHERE id = sl.id;

    -- Loga acesso (best-effort)
    BEGIN
        INSERT INTO document_access_logs (document_id, tenant_id, action, share_link_id)
        SELECT sl.document_id, d.tenant_id, 'shared_link_access', sl.id
        FROM documents d WHERE d.id = sl.document_id;
    EXCEPTION WHEN OTHERS THEN
        -- não derruba a request se log falhar (schema pode variar)
        NULL;
    END;

    SELECT * INTO v_doc FROM documents WHERE id = sl.document_id;

    RETURN jsonb_build_object(
        'document_id',    sl.document_id,
        'bucket',         v_doc.storage_bucket,
        'bucket_path',    v_doc.bucket_path,
        'nome_original',  v_doc.nome_original,
        'mime_type',      v_doc.mime_type,
        'tamanho_bytes',  v_doc.tamanho_bytes
    );
END;
$function$;

REVOKE ALL ON FUNCTION public.validate_share_token(text) FROM PUBLIC, authenticated;
GRANT  EXECUTE ON FUNCTION public.validate_share_token(text) TO anon, authenticated, service_role;

-- Restringe SELECT direto da tabela: só criador (saas_admin via outra policy se necessário)
DROP POLICY IF EXISTS "dsl: public read by token" ON public.document_share_links;
DROP POLICY IF EXISTS "dsl: creator full access"  ON public.document_share_links;

CREATE POLICY "dsl: creator full access" ON public.document_share_links
  FOR ALL TO authenticated
  USING (criado_por = auth.uid() OR public.is_saas_admin())
  WITH CHECK (criado_por = auth.uid());

-- ─────────────────────────────────────────────────────────────────────────
-- V#47: document_signatures — separar SELECT/INSERT (tenant_member) vs UPDATE/DELETE (signatário)
-- -----------------------------------------------------------------------------
DROP POLICY IF EXISTS "ds: tenant members access" ON public.document_signatures;
DROP POLICY IF EXISTS "ds: select" ON public.document_signatures;
DROP POLICY IF EXISTS "ds: insert" ON public.document_signatures;
DROP POLICY IF EXISTS "ds: update" ON public.document_signatures;
DROP POLICY IF EXISTS "ds: delete" ON public.document_signatures;

CREATE POLICY "ds: select" ON public.document_signatures
  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'
    )
  );

-- INSERT: tenant_member pode criar; signatario_id (se preenchido) deve ser o caller
-- (paciente externo é signatario_tipo='paciente' com signatario_id NULL — a row
-- nasce sem assinatura e signatario_id é preenchido na aceitação via outro fluxo)
CREATE POLICY "ds: insert" ON public.document_signatures
  FOR INSERT TO authenticated
  WITH CHECK (
    tenant_id IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
    AND (signatario_id IS NULL OR signatario_id = auth.uid())
  );

-- UPDATE: só o signatário designado ou saas_admin (impede secretária forjar status='assinado')
CREATE POLICY "ds: update" ON public.document_signatures
  FOR UPDATE TO authenticated
  USING (signatario_id = auth.uid() OR public.is_saas_admin())
  WITH CHECK (signatario_id = auth.uid() OR public.is_saas_admin());

-- DELETE: signatário, saas_admin ou tenant_admin/owner
CREATE POLICY "ds: delete" ON public.document_signatures
  FOR DELETE TO authenticated
  USING (
    signatario_id = auth.uid()
    OR 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'
          AND tm.role IN ('tenant_admin','admin','owner')
    )
  );

-- ─────────────────────────────────────────────────────────────────────────
-- V#48: document_access_logs — INSERT com WITH CHECK
-- -----------------------------------------------------------------------------
DROP POLICY IF EXISTS "dal: tenant members can insert" ON public.document_access_logs;
CREATE POLICY "dal: tenant members can insert" ON public.document_access_logs
  FOR INSERT TO authenticated
  WITH CHECK (
    tenant_id IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
  );

-- ─────────────────────────────────────────────────────────────────────────
-- V#49: document_templates — INSERT com WITH CHECK
-- -----------------------------------------------------------------------------
DROP POLICY IF EXISTS "dt: owner can insert"             ON public.document_templates;
DROP POLICY IF EXISTS "dt: saas admin can insert global" ON public.document_templates;

CREATE POLICY "dt: owner can insert" ON public.document_templates
  FOR INSERT TO authenticated
  WITH CHECK (
    is_global = false
    AND owner_id = auth.uid()
    AND tenant_id IN (
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = auth.uid() AND tm.status = 'active'
    )
  );

CREATE POLICY "dt: saas admin can insert global" ON public.document_templates
  FOR INSERT TO authenticated
  WITH CHECK (is_global = true AND public.is_saas_admin());
