-- =============================================================================
-- F6.2 Lote D — RPCs user-facing roteadas pro schema do tenant
--
-- ⚠️ APLICAR COMO supabase_admin (mix de funções owned postgres/supabase_admin).
--   docker exec -i -e PGPASSWORD=postgres supabase_db_agenciapsi-primesakai \
--     psql -U supabase_admin -h 127.0.0.1 -d postgres -v ON_ERROR_STOP=1 \
--     < database-novo/manual/f6_2d_user_rpcs.supabase_admin.sql
--
-- Padrão: valida is_tenant_member(p_tenant_id) + set_config search_path pro
-- schema do tenant; remove `WHERE tenant_id=` e tenant_id de inserts; unqualify
-- tabelas tenant; %ROWTYPE→RECORD; RETURNS <tabela_tenant>→jsonb.
-- Tabelas que FICAM em public (audit_logs global; patient_intake_requests,
-- document_share_links F1b) seguem com `public.` + filtro tenant_id.
--
-- list_my_signatures é CROSS-TENANT (assinante em vários tenants) → Lote F.
-- =============================================================================

BEGIN;

-- helper: valida acesso e RETORNA o schema do tenant. NÃO seta search_path
-- (set_config feito dentro de helper com SET search_path próprio seria revertido
-- na saída do helper). Cada RPC faz: PERFORM set_config('search_path',
-- public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
CREATE OR REPLACE FUNCTION public._tenant_route(p_tenant_id uuid)
RETURNS text LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_schema text;
BEGIN
    IF p_tenant_id IS NULL THEN RAISE EXCEPTION 'p_tenant_id obrigatório'; END IF;
    IF NOT public.is_tenant_member(p_tenant_id) AND NOT public.is_saas_admin() THEN
        RAISE EXCEPTION 'Sem permissão no tenant %', p_tenant_id USING ERRCODE='42501';
    END IF;
    v_schema := public.tenant_schema_for(p_tenant_id);
    IF v_schema IS NULL THEN RAISE EXCEPTION 'schema não encontrado p/ tenant %', p_tenant_id; END IF;
    RETURN v_schema;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- GRUPO 1 — já têm p_tenant_id, RETURNS jsonb/void (CREATE OR REPLACE)
-- ───────────────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_is_native boolean; v_fields int:=0; v_logs int:=0; v_parent int:=0;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT dc.is_native INTO v_is_native FROM determined_commitments dc WHERE dc.id = p_commitment_id;
    IF v_is_native IS NULL THEN RAISE EXCEPTION 'Commitment not found'; END IF;
    IF v_is_native = true THEN RAISE EXCEPTION 'Cannot delete native commitment'; END IF;
    DELETE FROM determined_commitment_fields WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_fields = ROW_COUNT;
    DELETE FROM commitment_time_logs WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_logs = ROW_COUNT;
    DELETE FROM determined_commitments WHERE id = p_commitment_id; GET DIAGNOSTICS v_parent = ROW_COUNT;
    IF v_parent <> 1 THEN RAISE EXCEPTION 'Parent not deleted'; END IF;
    RETURN jsonb_build_object('ok',true,'deleted',jsonb_build_object('fields',v_fields,'logs',v_logs,'commitment',v_parent));
END $$;

CREATE OR REPLACE FUNCTION public.delete_determined_commitment(p_tenant_id uuid, p_commitment_id uuid)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_is_native boolean; v_fields int:=0; v_logs int:=0; v_parent int:=0;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT dc.is_native INTO v_is_native FROM determined_commitments dc WHERE dc.id = p_commitment_id;
    IF v_is_native IS NULL THEN RAISE EXCEPTION 'Commitment not found for tenant'; END IF;
    IF v_is_native = true THEN RAISE EXCEPTION 'Cannot delete native commitment'; END IF;
    DELETE FROM determined_commitment_fields WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_fields = ROW_COUNT;
    DELETE FROM commitment_time_logs WHERE commitment_id = p_commitment_id; GET DIAGNOSTICS v_logs = ROW_COUNT;
    DELETE FROM determined_commitments WHERE id = p_commitment_id; GET DIAGNOSTICS v_parent = ROW_COUNT;
    IF v_parent <> 1 THEN RAISE EXCEPTION 'Delete did not remove the commitment'; END IF;
    RETURN jsonb_build_object('ok',true,'tenant_id',p_tenant_id,'commitment_id',p_commitment_id,
        'deleted',jsonb_build_object('fields',v_fields,'logs',v_logs,'commitment',v_parent));
END $$;

CREATE OR REPLACE FUNCTION public.seed_default_patient_groups(p_tenant_id uuid)
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_owner_id uuid; v_schema text;
BEGIN
    v_schema := public.tenant_schema_for(p_tenant_id);
    IF v_schema IS NULL THEN RETURN; END IF;  -- schema ainda não existe (chamado antes do clone): no-op
    SELECT user_id INTO v_owner_id FROM public.tenant_members
    WHERE tenant_id = p_tenant_id AND role='tenant_admin' AND status='active' LIMIT 1;
    IF v_owner_id IS NULL THEN RETURN; END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    INSERT INTO patient_groups (owner_id, nome, cor, is_system)
    VALUES (v_owner_id,'Crianças','#60a5fa',true),
           (v_owner_id,'Adolescentes','#a78bfa',true),
           (v_owner_id,'Idosos','#34d399',true)
    ON CONFLICT (owner_id, nome) DO NOTHING;
END $$;

-- seed_determined_commitments: idêntico em estrutura, sem tenant_id nos inserts.
-- Recriado integralmente.
CREATE OR REPLACE FUNCTION public.seed_determined_commitments(p_tenant_id uuid)
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_id uuid; v_schema text;
BEGIN
    v_schema := public.tenant_schema_for(p_tenant_id);
    IF v_schema IS NULL THEN RETURN; END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);

    IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='session') THEN
        INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
        VALUES (true,'session',true,true,'Sessão','Sessão com paciente');
    END IF;
    IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='reading') THEN
        INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
        VALUES (true,'reading',false,true,'Leitura','Praticar leitura');
    END IF;
    IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='supervision') THEN
        INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
        VALUES (true,'supervision',false,true,'Supervisão','Supervisão');
    END IF;
    IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='class') THEN
        INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
        VALUES (true,'class',false,false,'Aula','Dar aula');
    END IF;
    IF NOT EXISTS (SELECT 1 FROM determined_commitments WHERE is_native=true AND native_key='analysis') THEN
        INSERT INTO determined_commitments (is_native, native_key, is_locked, active, name, description)
        VALUES (true,'analysis',false,true,'Análise Pessoal','Minha análise pessoal');
    END IF;

    SELECT id INTO v_id FROM determined_commitments WHERE is_native=true AND native_key='session' LIMIT 1;
    IF v_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='notes') THEN
        INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order)
        VALUES (v_id,'notes','Observação','textarea',false,30);
    END IF;
    SELECT id INTO v_id FROM determined_commitments WHERE is_native=true AND native_key='reading' LIMIT 1;
    IF v_id IS NOT NULL THEN
        IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='book') THEN
            INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'book','Livro','text',false,10);
        END IF;
        IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='author') THEN
            INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'author','Autor','text',false,20);
        END IF;
        IF NOT EXISTS (SELECT 1 FROM determined_commitment_fields WHERE commitment_id=v_id AND key='notes') THEN
            INSERT INTO determined_commitment_fields (commitment_id, key, label, field_type, required, sort_order) VALUES (v_id,'notes','Observação','textarea',false,30);
        END IF;
    END IF;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- GRUPO 2 — novo p_tenant_id (1º param), RETURNS scalar/jsonb (DROP+CREATE)
-- ───────────────────────────────────────────────────────────────────────────

DROP FUNCTION IF EXISTS public.cancel_recurrence_from(uuid, date);
CREATE FUNCTION public.cancel_recurrence_from(p_tenant_id uuid, p_recurrence_id uuid, p_from_date date)
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    UPDATE recurrence_rules
    SET end_date = p_from_date - INTERVAL '1 day', open_ended = false,
        status = CASE WHEN p_from_date <= start_date THEN 'cancelado' ELSE status END,
        updated_at = now()
    WHERE id = p_recurrence_id;
END $$;

DROP FUNCTION IF EXISTS public.cancelar_eventos_serie(uuid, timestamptz);
CREATE FUNCTION public.cancelar_eventos_serie(p_tenant_id uuid, p_serie_id uuid, p_a_partir_de timestamptz DEFAULT now())
RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_count integer;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    UPDATE agenda_eventos SET status='cancelado', updated_at=now()
    WHERE serie_id = p_serie_id AND inicio_em >= p_a_partir_de AND status NOT IN ('realizado','cancelado');
    GET DIAGNOSTICS v_count = ROW_COUNT;
    RETURN v_count;
END $$;

DROP FUNCTION IF EXISTS public.split_recurrence_at(uuid, date);
CREATE FUNCTION public.split_recurrence_at(p_tenant_id uuid, p_recurrence_id uuid, p_from_date date)
RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_old RECORD; v_new_id uuid;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT * INTO v_old FROM recurrence_rules WHERE id = p_recurrence_id;
    IF NOT FOUND THEN RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id; END IF;
    UPDATE recurrence_rules SET end_date = p_from_date - INTERVAL '1 day', open_ended=false, updated_at=now()
    WHERE id = p_recurrence_id;
    INSERT INTO recurrence_rules (owner_id, therapist_id, patient_id, determined_commitment_id, type, interval, weekdays,
        start_time, end_time, timezone, duration_min, start_date, end_date, max_occurrences, open_ended,
        modalidade, titulo_custom, observacoes, extra_fields, status)
    SELECT owner_id, therapist_id, patient_id, determined_commitment_id, type, interval, weekdays,
        start_time, end_time, timezone, duration_min, p_from_date, v_old.end_date, v_old.max_occurrences, v_old.open_ended,
        modalidade, titulo_custom, observacoes, extra_fields, status
    FROM recurrence_rules WHERE id = p_recurrence_id
    RETURNING id INTO v_new_id;
    RETURN v_new_id;
END $$;

-- can_delete_patient: SQL sem SET search_path → herda o do chamador (schema).
-- Unqualified pra resolver no schema do tenant ativo.
CREATE OR REPLACE FUNCTION public.can_delete_patient(p_patient_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
AS $$
    SELECT NOT EXISTS (
        SELECT 1 FROM agenda_eventos    WHERE patient_id = p_patient_id
        UNION ALL
        SELECT 1 FROM recurrence_rules  WHERE patient_id = p_patient_id
        UNION ALL
        SELECT 1 FROM billing_contracts WHERE patient_id = p_patient_id
    );
$$;

DROP FUNCTION IF EXISTS public.safe_delete_patient(uuid);
CREATE FUNCTION public.safe_delete_patient(p_tenant_id uuid, p_patient_id uuid)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    IF NOT public.can_delete_patient(p_patient_id) THEN
        RETURN jsonb_build_object('ok',false,'error','has_history',
            'message','Este paciente possui histórico clínico ou financeiro e não pode ser removido. Você pode desativar ou arquivar o paciente.');
    END IF;
    -- ownership: owner_id direto ou responsible_member do caller (tenant_members é GLOBAL)
    IF NOT EXISTS (SELECT 1 FROM patients
        WHERE id = p_patient_id AND (owner_id = auth.uid()
            OR responsible_member_id IN (SELECT id FROM public.tenant_members WHERE user_id = auth.uid()))) THEN
        RETURN jsonb_build_object('ok',false,'error','forbidden','message','Sem permissão para excluir este paciente.');
    END IF;
    DELETE FROM patients WHERE id = p_patient_id;
    RETURN jsonb_build_object('ok',true);
END $$;

DROP FUNCTION IF EXISTS public.export_patient_data(uuid);
CREATE FUNCTION public.export_patient_data(p_tenant_id uuid, p_patient_id uuid)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_patient RECORD; v_caller uuid; v_result jsonb;
BEGIN
    v_caller := auth.uid();
    IF v_caller IS NULL THEN RAISE EXCEPTION 'Autenticacao obrigatoria' USING ERRCODE='28000'; END IF;
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT * INTO v_patient FROM patients WHERE id = p_patient_id;
    IF NOT FOUND THEN RAISE EXCEPTION 'Paciente nao encontrado' USING ERRCODE='P0002'; END IF;
    v_result := jsonb_build_object(
        'export_metadata', jsonb_build_object('generated_at', now(), 'generated_by', v_caller,
            'tenant_id', p_tenant_id, 'patient_id', p_patient_id,
            'lgpd_basis','Art. 18, II - portabilidade dos dados do titular',
            'controller','AgenciaPSI - Clinica responsavel','format_version','1.0'),
        'paciente', to_jsonb(v_patient),
        'contatos', COALESCE((SELECT jsonb_agg(to_jsonb(pc) ORDER BY pc.created_at) FROM patient_contacts pc WHERE pc.patient_id = p_patient_id),'[]'::jsonb),
        'contatos_apoio', COALESCE((SELECT jsonb_agg(to_jsonb(psc) ORDER BY psc.created_at) FROM patient_support_contacts psc WHERE psc.patient_id = p_patient_id),'[]'::jsonb),
        'historico_status', COALESCE((SELECT jsonb_agg(to_jsonb(psh) ORDER BY psh.alterado_em) FROM patient_status_history psh WHERE psh.patient_id = p_patient_id),'[]'::jsonb),
        'timeline', COALESCE((SELECT jsonb_agg(to_jsonb(pt) ORDER BY pt.ocorrido_em) FROM patient_timeline pt WHERE pt.patient_id = p_patient_id),'[]'::jsonb),
        'descontos', COALESCE((SELECT jsonb_agg(to_jsonb(pd) ORDER BY pd.created_at) FROM patient_discounts pd WHERE pd.patient_id = p_patient_id),'[]'::jsonb),
        'eventos_agenda', COALESCE((SELECT jsonb_agg(jsonb_build_object('id',ae.id,'tipo',ae.tipo,'status',ae.status,'inicio_em',ae.inicio_em,'fim_em',ae.fim_em) ORDER BY ae.inicio_em) FROM agenda_eventos ae WHERE ae.patient_id = p_patient_id),'[]'::jsonb),
        'documentos', COALESCE((SELECT jsonb_agg(jsonb_build_object('id',d.id,'nome',d.nome_original,'tipo',d.tipo_documento,'criado_em',d.created_at) ORDER BY d.created_at) FROM documents d WHERE d.patient_id = p_patient_id AND d.deleted_at IS NULL),'[]'::jsonb),
        'financeiro', COALESCE((SELECT jsonb_agg(jsonb_build_object('id',fr.id,'tipo',fr.type,'valor',fr.final_amount,'status',fr.status,'vencimento',fr.due_date) ORDER BY fr.created_at) FROM financial_records fr WHERE fr.patient_id = p_patient_id AND fr.deleted_at IS NULL),'[]'::jsonb)
    );
    RETURN v_result;
END $$;

DROP FUNCTION IF EXISTS public.search_global(text, text[], integer);
CREATE FUNCTION public.search_global(p_tenant_id uuid, p_q text, p_scope text[] DEFAULT NULL, p_limit integer DEFAULT 8)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
    v_q text; v_pattern text; v_limit int;
    v_patients jsonb:='[]'::jsonb; v_appointments jsonb:='[]'::jsonb; v_documents jsonb:='[]'::jsonb;
    v_services jsonb:='[]'::jsonb; v_intakes jsonb:='[]'::jsonb;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    v_q := nullif(btrim(coalesce(p_q,'')),'');
    IF v_q IS NULL OR length(v_q) < 2 THEN
        RETURN jsonb_build_object('patients','[]'::jsonb,'appointments','[]'::jsonb,'documents','[]'::jsonb,'services','[]'::jsonb,'intakes','[]'::jsonb);
    END IF;
    v_q := left(v_q,80); v_pattern := '%'||v_q||'%'; v_limit := GREATEST(1, LEAST(coalesce(p_limit,8),20));

    IF p_scope IS NULL OR 'patients' = ANY(p_scope) THEN
        SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',nome_completo,
            'sublabel',coalesce(nullif(email_principal,''),nullif(telefone,''),''),'avatar_url',avatar_url,
            'deeplink','/therapist/patients/cadastro/'||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_patients
        FROM (SELECT p.id,p.nome_completo,p.email_principal,p.telefone,p.avatar_url,
                GREATEST(similarity(coalesce(p.nome_completo,''),v_q),similarity(coalesce(p.email_principal,''),v_q)*0.7,
                    similarity(coalesce(p.telefone,''),v_q)*0.5,similarity(coalesce(p.cpf,''),v_q)*0.6) AS score
            FROM patients p WHERE p.nome_completo ILIKE v_pattern OR p.email_principal ILIKE v_pattern OR p.telefone ILIKE v_pattern OR p.cpf ILIKE v_pattern
            ORDER BY score DESC, p.nome_completo ASC LIMIT v_limit) ranked;
    END IF;
    IF p_scope IS NULL OR 'appointments' = ANY(p_scope) THEN
        SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',label,
            'sublabel',trim(both ' · ' from coalesce(patient_name,')')||' · '||to_char(inicio_em,'DD/MM/YYYY HH24:MI')),
            'deeplink','/therapist/agenda?event='||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_appointments
        FROM (SELECT e.id, coalesce(nullif(e.titulo_custom,''),nullif(e.titulo,''),'Sessão') AS label, e.inicio_em, pat.nome_completo AS patient_name,
                GREATEST(similarity(coalesce(e.titulo,''),v_q),similarity(coalesce(e.titulo_custom,''),v_q),similarity(coalesce(pat.nome_completo,''),v_q)*0.9) AS score
            FROM agenda_eventos e LEFT JOIN patients pat ON pat.id = e.patient_id
            WHERE e.titulo ILIKE v_pattern OR e.titulo_custom ILIKE v_pattern OR pat.nome_completo ILIKE v_pattern
            ORDER BY score DESC, e.inicio_em DESC LIMIT v_limit) ranked;
    END IF;
    IF p_scope IS NULL OR 'documents' = ANY(p_scope) THEN
        SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',nome_original,
            'sublabel',trim(both ' · ' from coalesce(patient_name,'')||' · '||coalesce(tipo_documento,'')),
            'deeplink','/therapist/patients/'||patient_id::text||'/documents','score',round(score::numeric,3))),'[]'::jsonb) INTO v_documents
        FROM (SELECT d.id,d.patient_id,d.nome_original,d.tipo_documento,pat.nome_completo AS patient_name,
                GREATEST(similarity(coalesce(d.nome_original,''),v_q),similarity(coalesce(d.descricao,''),v_q)*0.7) AS score
            FROM documents d LEFT JOIN patients pat ON pat.id = d.patient_id
            WHERE d.nome_original ILIKE v_pattern OR d.descricao ILIKE v_pattern
            ORDER BY score DESC, d.nome_original ASC LIMIT v_limit) ranked;
    END IF;
    IF p_scope IS NULL OR 'services' = ANY(p_scope) THEN
        SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,'label',name,
            'sublabel',trim(both ' · ' from 'R$ '||to_char(price,'FM999G999G990D00')||' · '||coalesce(duration_min::text||' min','')),
            'deeplink','/configuracoes/precificacao','score',round(score::numeric,3))),'[]'::jsonb) INTO v_services
        FROM (SELECT s.id,s.name,s.price,s.duration_min,
                GREATEST(similarity(coalesce(s.name,''),v_q),similarity(coalesce(s.description,''),v_q)*0.7) AS score
            FROM services s WHERE s.active IS TRUE AND (s.name ILIKE v_pattern OR s.description ILIKE v_pattern)
            ORDER BY score DESC, s.name ASC LIMIT v_limit) ranked;
    END IF;
    -- intakes: patient_intake_requests FICA em public (F1b) → qualifica + filtra tenant_id
    IF p_scope IS NULL OR 'intakes' = ANY(p_scope) THEN
        SELECT coalesce(jsonb_agg(jsonb_build_object('id',id,
            'label',coalesce(nullif(trim(nome_completo),''),'(sem nome)'),
            'sublabel',trim(both ' · ' from coalesce(nullif(email_principal,''),nullif(telefone,''),'')||' · '||'recebido '||to_char(created_at,'DD/MM/YYYY')),
            'deeplink','/therapist/patients/cadastro/recebidos?id='||id::text,'score',round(score::numeric,3))),'[]'::jsonb) INTO v_intakes
        FROM (SELECT r.id,r.nome_completo,r.email_principal,r.telefone,r.created_at,
                GREATEST(similarity(coalesce(r.nome_completo,''),v_q),similarity(coalesce(r.email_principal,''),v_q)*0.7,similarity(coalesce(r.telefone,''),v_q)*0.5) AS score
            FROM public.patient_intake_requests r
            WHERE r.tenant_id = p_tenant_id AND r.status='new'
              AND (r.nome_completo ILIKE v_pattern OR r.email_principal ILIKE v_pattern OR r.telefone ILIKE v_pattern)
            ORDER BY score DESC, r.created_at DESC LIMIT v_limit) ranked;
    END IF;

    RETURN jsonb_build_object('patients',v_patients,'appointments',v_appointments,'documents',v_documents,'services',v_services,'intakes',v_intakes);
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- GRUPO 3 — RETURNS <tabela_tenant> → jsonb (ripple no FE)
-- ───────────────────────────────────────────────────────────────────────────

DROP FUNCTION IF EXISTS public.mark_as_paid(uuid, text);
CREATE FUNCTION public.mark_as_paid(p_tenant_id uuid, p_financial_record_id uuid, p_payment_method text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_record RECORD;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT * INTO v_record FROM financial_records WHERE id = p_financial_record_id AND owner_id = auth.uid() AND deleted_at IS NULL;
    IF NOT FOUND THEN RAISE EXCEPTION 'Registro financeiro não encontrado ou sem permissão.'; END IF;
    IF v_record.status NOT IN ('pending','overdue') THEN RAISE EXCEPTION 'Apenas cobranças pendentes ou vencidas podem ser marcadas como pagas.'; END IF;
    UPDATE financial_records SET status='paid', paid_at=now(), payment_method=p_payment_method, updated_at=now()
    WHERE id = p_financial_record_id RETURNING * INTO v_record;
    RETURN to_jsonb(v_record);
END $$;

DROP FUNCTION IF EXISTS public.create_financial_record_for_session(uuid, uuid, uuid, uuid, numeric, date);
CREATE FUNCTION public.create_financial_record_for_session(p_tenant_id uuid, p_owner_id uuid, p_patient_id uuid, p_agenda_evento_id uuid, p_amount numeric, p_due_date date)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_existing RECORD; v_new RECORD;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT * INTO v_existing FROM financial_records WHERE agenda_evento_id = p_agenda_evento_id AND deleted_at IS NULL AND status != 'cancelled' LIMIT 1;
    IF FOUND THEN RETURN to_jsonb(v_existing); END IF;
    INSERT INTO financial_records (owner_id, patient_id, agenda_evento_id, amount, discount_amount, final_amount, status, due_date)
    VALUES (p_owner_id, p_patient_id, p_agenda_evento_id, p_amount, 0, p_amount, 'pending', p_due_date)
    RETURNING * INTO v_new;
    UPDATE agenda_eventos SET billed = TRUE WHERE id = p_agenda_evento_id;
    RETURN to_jsonb(v_new);
END $$;

DROP FUNCTION IF EXISTS public.mark_payout_as_paid(uuid);
CREATE FUNCTION public.mark_payout_as_paid(p_tenant_id uuid, p_payout_id uuid)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_payout RECORD;
BEGIN
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    SELECT * INTO v_payout FROM therapist_payouts WHERE id = p_payout_id;
    IF NOT FOUND THEN RAISE EXCEPTION 'Repasse não encontrado: %', p_payout_id; END IF;
    IF NOT public.is_tenant_admin(p_tenant_id) THEN RAISE EXCEPTION 'Apenas o administrador da clínica pode marcar repasses como pagos.'; END IF;
    IF v_payout.status <> 'pending' THEN RAISE EXCEPTION 'Repasse já está com status ''%''. Apenas repasses pendentes podem ser pagos.', v_payout.status; END IF;
    UPDATE therapist_payouts SET status='paid', paid_at=now(), updated_at=now() WHERE id = p_payout_id RETURNING * INTO v_payout;
    RETURN to_jsonb(v_payout);
END $$;

DROP FUNCTION IF EXISTS public.create_therapist_payout(uuid, uuid, date, date);
CREATE FUNCTION public.create_therapist_payout(p_tenant_id uuid, p_therapist_id uuid, p_period_start date, p_period_end date)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_payout RECORD; v_total int; v_gross numeric(10,2); v_clinic_fee numeric(10,2); v_net numeric(10,2);
BEGIN
    IF auth.uid() <> p_therapist_id AND NOT public.is_tenant_admin(p_tenant_id) THEN
        RAISE EXCEPTION 'Sem permissão para criar repasse para este terapeuta.';
    END IF;
    PERFORM set_config('search_path', public._tenant_route(p_tenant_id) || ',public,pg_temp', true);
    IF EXISTS (SELECT 1 FROM therapist_payouts WHERE owner_id=p_therapist_id AND period_start=p_period_start AND period_end=p_period_end AND status<>'cancelled') THEN
        RAISE EXCEPTION 'Já existe um repasse ativo para o período % a % deste terapeuta.', p_period_start, p_period_end;
    END IF;
    SELECT COUNT(*), COALESCE(SUM(amount),0), COALESCE(SUM(clinic_fee_amount),0), COALESCE(SUM(net_amount),0)
    INTO v_total, v_gross, v_clinic_fee, v_net
    FROM financial_records fr
    WHERE fr.owner_id=p_therapist_id AND fr.type='receita' AND fr.status='paid' AND fr.deleted_at IS NULL
      AND fr.paid_at::date BETWEEN p_period_start AND p_period_end
      AND NOT EXISTS (SELECT 1 FROM therapist_payout_records tpr WHERE tpr.financial_record_id = fr.id);
    IF v_total = 0 THEN RAISE EXCEPTION 'Nenhum registro financeiro elegível encontrado para o período % a %.', p_period_start, p_period_end; END IF;
    INSERT INTO therapist_payouts (owner_id, period_start, period_end, total_sessions, gross_amount, clinic_fee_total, net_amount, status)
    VALUES (p_therapist_id, p_period_start, p_period_end, v_total, v_gross, v_clinic_fee, v_net, 'pending')
    RETURNING * INTO v_payout;
    INSERT INTO therapist_payout_records (payout_id, financial_record_id)
    SELECT v_payout.id, fr.id FROM financial_records fr
    WHERE fr.owner_id=p_therapist_id AND fr.type='receita' AND fr.status='paid' AND fr.deleted_at IS NULL
      AND fr.paid_at::date BETWEEN p_period_start AND p_period_end
      AND NOT EXISTS (SELECT 1 FROM therapist_payout_records tpr WHERE tpr.financial_record_id = fr.id);
    RETURN to_jsonb(v_payout);
END $$;

COMMIT;
