-- =============================================================================
-- F6.2 Lote F — RPCs anon/token: resolvem tenant por token/slug e roteiam
--
-- ⚠️ APLICAR COMO supabase_admin.
--
-- Visitante anon não está logado → cada RPC resolve o tenant a partir do
-- token/slug do registro que VIVE em public (F1b: document_share_links,
-- agendador_configuracoes — ambos têm tenant_id), depois set_config search_path
-- pro schema só pras tabelas tenant (documents, document_signatures,
-- document_access_logs, patients, agenda_*, recurrence_*).
-- Tabelas que ficam em public seguem qualificadas (document_share_links,
-- agendador_configuracoes/solicitacoes).
-- %ROWTYPE de tabelas tenant → RECORD; RETURNS document_signatures → jsonb.
-- list_my_signatures é cross-tenant (assinante em vários tenants) → fan-out.
-- =============================================================================

BEGIN;

-- ── Documentos: tenant via document_share_links.tenant_id (public) ──────────

CREATE OR REPLACE FUNCTION public.validate_share_token(p_token text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE sl public.document_share_links%ROWTYPE; v_doc RECORD; v_token text; v_schema 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 public.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;
    v_schema := public.tenant_schema_for(sl.tenant_id);
    IF v_schema IS NULL THEN RAISE EXCEPTION 'tenant inválido' USING ERRCODE='28000'; END IF;

    UPDATE public.document_share_links SET usos = usos + 1 WHERE id = sl.id;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    BEGIN
        INSERT INTO document_access_logs (documento_id, action, share_link_id)
        VALUES (sl.documento_id, 'shared_link_access', sl.id);
    EXCEPTION WHEN OTHERS THEN NULL; END;
    SELECT * INTO v_doc FROM documents WHERE id = sl.documento_id;
    RETURN jsonb_build_object('document_id', sl.documento_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 $$;

CREATE OR REPLACE FUNCTION public.get_signable_document_by_token(p_token text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_link public.document_share_links%ROWTYPE; v_doc RECORD; v_sigs jsonb; v_schema text;
BEGIN
    IF p_token IS NULL OR length(p_token) < 32 THEN RAISE EXCEPTION 'Token inválido' USING ERRCODE='22023'; END IF;
    SELECT * INTO v_link FROM public.document_share_links WHERE token = p_token AND ativo=true AND expira_em > now() AND usos < usos_max LIMIT 1;
    IF v_link.id IS NULL THEN RETURN jsonb_build_object('valid', false, 'error', 'expired_or_invalid'); END IF;
    v_schema := public.tenant_schema_for(v_link.tenant_id);
    IF v_schema IS NULL THEN RETURN jsonb_build_object('valid', false, 'error', 'tenant_invalid'); END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    SELECT * INTO v_doc FROM documents WHERE id = v_link.documento_id AND deleted_at IS NULL LIMIT 1;
    IF v_doc.id IS NULL THEN RETURN jsonb_build_object('valid', false, 'error', 'document_not_found'); END IF;
    SELECT jsonb_agg(jsonb_build_object('id',s.id,'signatario_tipo',s.signatario_tipo,'signatario_nome',s.signatario_nome,
        'signatario_email',s.signatario_email,'ordem',s.ordem,'status',s.status,'assinado_em',s.assinado_em) ORDER BY s.ordem) INTO v_sigs
    FROM document_signatures s WHERE s.documento_id = v_doc.id;
    RETURN jsonb_build_object('valid', true,
        'document', jsonb_build_object('id',v_doc.id,'nome_original',v_doc.nome_original,'mime_type',v_doc.mime_type,
            'tamanho_bytes',v_doc.tamanho_bytes,'bucket_path',v_doc.bucket_path,'storage_bucket',v_doc.storage_bucket,'tipo_documento',v_doc.tipo_documento),
        'signatures', COALESCE(v_sigs,'[]'::jsonb), 'expira_em', v_link.expira_em, 'usos_restantes', v_link.usos_max - v_link.usos);
END $$;

DROP FUNCTION IF EXISTS public.sign_document_by_token(text, uuid, text);
CREATE FUNCTION public.sign_document_by_token(p_token text, p_signature_id uuid DEFAULT NULL, p_hash_documento text DEFAULT NULL)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_link public.document_share_links%ROWTYPE; v_sig RECORD; v_ip inet; v_ua text; v_schema text;
BEGIN
    IF p_token IS NULL OR length(p_token) < 32 THEN RAISE EXCEPTION 'Token inválido' USING ERRCODE='22023'; END IF;
    SELECT * INTO v_link FROM public.document_share_links WHERE token = p_token AND ativo=true AND expira_em > now() AND usos < usos_max LIMIT 1;
    IF v_link.id IS NULL THEN RAISE EXCEPTION 'Link expirado, inválido ou esgotado' USING ERRCODE='P0002'; END IF;
    v_schema := public.tenant_schema_for(v_link.tenant_id);
    IF v_schema IS NULL THEN RAISE EXCEPTION 'tenant inválido' USING ERRCODE='P0002'; END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    IF p_signature_id IS NOT NULL THEN
        SELECT * INTO v_sig FROM document_signatures WHERE id = p_signature_id AND documento_id = v_link.documento_id AND status IN ('pendente','enviado') LIMIT 1;
    ELSE
        SELECT * INTO v_sig FROM document_signatures WHERE documento_id = v_link.documento_id AND status IN ('pendente','enviado') ORDER BY ordem ASC, criado_em ASC LIMIT 1;
    END IF;
    IF v_sig.id IS NULL THEN RAISE EXCEPTION 'Nenhuma assinatura pendente para este documento' USING ERRCODE='P0002'; END IF;
    v_ip := inet_client_addr();
    BEGIN v_ua := current_setting('request.headers', true)::json ->> 'user-agent'; EXCEPTION WHEN OTHERS THEN v_ua := NULL; END;
    UPDATE document_signatures SET status='assinado', ip=v_ip, user_agent=v_ua, assinado_em=now(),
        hash_documento=COALESCE(p_hash_documento, hash_documento), atualizado_em=now()
    WHERE id = v_sig.id RETURNING * INTO v_sig;
    UPDATE public.document_share_links SET usos = usos + 1 WHERE id = v_link.id;
    RETURN to_jsonb(v_sig);
END $$;

-- sign_document_by_signature_id: assinante LOGADO (paciente OU therapist) via
-- portal. Paciente NÃO é tenant_member → routing UNCHECKED (p_tenant_id vem do
-- FE, da própria assinatura listada). Autorização é por LINHA: só assina se for
-- o signatário (signatario_id = uid OU email do uid).
DROP FUNCTION IF EXISTS public.sign_document_by_signature_id(uuid, text);
DROP FUNCTION IF EXISTS public.sign_document_by_signature_id(uuid, uuid, text);
CREATE FUNCTION public.sign_document_by_signature_id(p_tenant_id uuid, p_signature_id uuid, p_hash_documento text DEFAULT NULL)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_row RECORD; v_ip inet; v_ua text; v_uid uuid; v_email text;
BEGIN
    IF p_signature_id IS NULL THEN RAISE EXCEPTION 'p_signature_id obrigatório' USING ERRCODE='22023'; END IF;
    v_uid := auth.uid();
    IF v_uid IS NULL THEN RAISE EXCEPTION 'Sessão inválida' USING ERRCODE='28000'; END IF;
    SELECT email INTO v_email FROM auth.users WHERE id = v_uid;
    PERFORM set_config('search_path', public._tenant_schema_unchecked(p_tenant_id) || ',public,pg_temp', true);
    v_ip := inet_client_addr();
    BEGIN v_ua := current_setting('request.headers', true)::json ->> 'user-agent'; EXCEPTION WHEN OTHERS THEN v_ua := NULL; END;
    UPDATE document_signatures SET status='assinado', ip=v_ip, user_agent=v_ua, assinado_em=now(),
        hash_documento=COALESCE(p_hash_documento, hash_documento), atualizado_em=now()
    WHERE id = p_signature_id AND status IN ('pendente','enviado')
      AND (signatario_id = v_uid OR signatario_email = v_email
           OR documento_id IN (SELECT d.id FROM documents d JOIN patients p ON p.id = d.patient_id WHERE p.user_id = v_uid))
    RETURNING * INTO v_row;
    IF v_row.id IS NULL THEN RAISE EXCEPTION 'Assinatura não encontrada, já processada, ou sem permissão' USING ERRCODE='P0002'; END IF;
    RETURN to_jsonb(v_row);
END $$;

-- list_my_signatures: cross-tenant. Fan-out por schema (tenant_id injetado do loop).
-- document_share_links é GLOBAL (public). Ordenação global é aproximada (por schema).
CREATE OR REPLACE FUNCTION public.list_my_signatures(p_status text[] DEFAULT NULL)
RETURNS TABLE(signature_id uuid, documento_id uuid, tenant_id uuid, signatario_tipo text, status text, ordem smallint, assinado_em timestamptz, criado_em timestamptz, nome_original text, tipo_documento text, mime_type text, share_token text, share_expira_em timestamptz)
LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_uid uuid; t record;
BEGIN
    v_uid := auth.uid();
    IF v_uid IS NULL THEN RAISE EXCEPTION 'Sessão inválida' USING ERRCODE='28000'; END IF;
    FOR t IN SELECT ts.tenant_id, ts.schema_name FROM public.tenant_schemas ts LOOP
        RETURN QUERY EXECUTE format(
            'SELECT s.id, s.documento_id, $2::uuid, s.signatario_tipo, s.status, s.ordem, s.assinado_em, s.criado_em, '
            || 'd.nome_original, d.tipo_documento, d.mime_type, sl.token, sl.expira_em '
            || 'FROM %1$I.document_signatures s '
            || 'JOIN %1$I.documents d ON d.id = s.documento_id AND d.deleted_at IS NULL '
            || 'LEFT JOIN LATERAL (SELECT token, expira_em FROM public.document_share_links WHERE documento_id = d.id AND ativo=true AND expira_em > now() AND usos < usos_max ORDER BY criado_em DESC LIMIT 1) sl ON true '
            || 'WHERE (s.signatario_id = $1 OR s.signatario_email = (SELECT email FROM auth.users WHERE id=$1) '
            || 'OR d.patient_id IN (SELECT p.id FROM %1$I.patients p WHERE p.user_id = $1)) '
            || 'AND ($3::text[] IS NULL OR s.status = ANY($3))',
            t.schema_name)
        USING v_uid, t.tenant_id, p_status;
    END LOOP;
END $$;

-- ── match_patient_by_phone: service (edge), p_tenant_id → unchecked ──────────
CREATE OR REPLACE FUNCTION public.match_patient_by_phone(p_tenant_id uuid, p_phone text)
RETURNS uuid LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_normalized text; v_patient_id uuid;
BEGIN
    v_normalized := public.normalize_phone_br(p_phone);
    IF v_normalized IS NULL OR length(v_normalized) < 10 THEN RETURN NULL; END IF;
    PERFORM set_config('search_path', public._tenant_schema_unchecked(p_tenant_id) || ',public,pg_temp', true);
    SELECT id INTO v_patient_id FROM patients WHERE public.normalize_phone_br(telefone) = v_normalized LIMIT 1;
    IF v_patient_id IS NOT NULL THEN RETURN v_patient_id; END IF;
    SELECT id INTO v_patient_id FROM patients WHERE public.normalize_phone_br(telefone_alternativo) = v_normalized LIMIT 1;
    IF v_patient_id IS NOT NULL THEN RETURN v_patient_id; END IF;
    SELECT id INTO v_patient_id FROM patients WHERE public.normalize_phone_br(telefone_responsavel) = v_normalized LIMIT 1;
    RETURN v_patient_id;
END $$;

-- ── Agendador público: tenant via agendador_configuracoes.tenant_id (public) ─
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer)
RETURNS TABLE(data date, tem_slots boolean) LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_owner_id uuid; v_tenant_id uuid; v_schema text; v_antecedencia int; v_agora timestamptz;
        v_data date; v_data_inicio date; v_data_fim date; v_db_dow int; v_tem_slot boolean; v_bloqueado boolean;
BEGIN
    SELECT c.owner_id, c.tenant_id, c.antecedencia_minima_horas INTO v_owner_id, v_tenant_id, v_antecedencia
    FROM public.agendador_configuracoes c WHERE c.link_slug = p_slug AND c.ativo = true LIMIT 1;
    IF v_owner_id IS NULL THEN RETURN; END IF;
    v_schema := public.tenant_schema_for(v_tenant_id);
    IF v_schema IS NULL THEN RETURN; END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    v_agora := now(); v_data_inicio := make_date(p_ano, p_mes, 1);
    v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date; v_data := v_data_inicio;
    WHILE v_data <= v_data_fim LOOP
        v_db_dow := extract(dow from v_data::timestamp)::int;
        SELECT EXISTS (SELECT 1 FROM agenda_bloqueios b WHERE b.owner_id=v_owner_id AND b.data_inicio<=v_data AND COALESCE(b.data_fim,v_data)>=v_data AND b.hora_inicio IS NULL AND ((NOT b.recorrente) OR (b.recorrente AND b.dia_semana=v_db_dow))) INTO v_bloqueado;
        IF v_bloqueado THEN v_data := v_data + 1; CONTINUE; END IF;
        SELECT EXISTS (SELECT 1 FROM agenda_online_slots s WHERE s.owner_id=v_owner_id AND s.weekday=v_db_dow AND s.enabled=true
            AND (v_data::text||' '||s.time::text)::timestamp AT TIME ZONE 'America/Sao_Paulo' >= v_agora + (v_antecedencia||' hours')::interval) INTO v_tem_slot;
        IF v_tem_slot THEN data := v_data; tem_slots := true; RETURN NEXT; END IF;
        v_data := v_data + 1;
    END LOOP;
END $$;

CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date)
RETURNS TABLE(hora time without time zone, disponivel boolean) LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_owner_id uuid; v_tenant_id uuid; v_schema text; v_duracao int; v_antecedencia int; v_agora timestamptz;
        v_db_dow int; v_slot time; v_slot_fim time; v_slot_ts timestamptz; v_ocupado boolean;
        v_rule RECORD; v_rule_start_dow int; v_first_occ date; v_day_diff int; v_ex_type text;
BEGIN
    SELECT c.owner_id, c.tenant_id, c.duracao_sessao_min, c.antecedencia_minima_horas
    INTO v_owner_id, v_tenant_id, v_duracao, v_antecedencia
    FROM public.agendador_configuracoes c WHERE c.link_slug = p_slug AND c.ativo = true LIMIT 1;
    IF v_owner_id IS NULL THEN RETURN; END IF;
    v_schema := public.tenant_schema_for(v_tenant_id);
    IF v_schema IS NULL THEN RETURN; END IF;
    PERFORM set_config('search_path', v_schema || ',public,pg_temp', true);
    v_agora := now(); v_db_dow := extract(dow from p_data::timestamp)::int;
    IF EXISTS (SELECT 1 FROM agenda_bloqueios b WHERE b.owner_id=v_owner_id AND b.data_inicio<=p_data AND COALESCE(b.data_fim,p_data)>=p_data AND b.hora_inicio IS NULL AND ((NOT b.recorrente) OR (b.recorrente AND b.dia_semana=v_db_dow))) THEN RETURN; END IF;
    FOR v_slot IN SELECT s.time FROM agenda_online_slots s WHERE s.owner_id=v_owner_id AND s.weekday=v_db_dow AND s.enabled=true ORDER BY s.time LOOP
        v_slot_fim := v_slot + (v_duracao||' minutes')::interval; v_ocupado := false;
        v_slot_ts := (p_data::text||' '||v_slot::text)::timestamp AT TIME ZONE 'America/Sao_Paulo';
        IF v_slot_ts < v_agora + (v_antecedencia||' hours')::interval THEN v_ocupado := true; END IF;
        IF NOT v_ocupado THEN
            SELECT EXISTS (SELECT 1 FROM agenda_bloqueios b WHERE b.owner_id=v_owner_id AND b.data_inicio<=p_data AND COALESCE(b.data_fim,p_data)>=p_data AND b.hora_inicio IS NOT NULL AND b.hora_inicio<v_slot_fim AND b.hora_fim>v_slot AND ((NOT b.recorrente) OR (b.recorrente AND b.dia_semana=v_db_dow))) INTO v_ocupado;
        END IF;
        IF NOT v_ocupado THEN
            SELECT EXISTS (SELECT 1 FROM agenda_eventos e WHERE e.owner_id=v_owner_id AND e.status::text NOT IN ('cancelado','faltou') AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::date=p_data AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::time<v_slot_fim AND (e.fim_em AT TIME ZONE 'America/Sao_Paulo')::time>v_slot) INTO v_ocupado;
        END IF;
        IF NOT v_ocupado THEN
            FOR v_rule IN SELECT r.id, r.start_date::date AS start_date, r.end_date::date AS end_date, r.start_time::time AS start_time, r.end_time::time AS end_time, COALESCE(r.interval,1)::int AS interval
                FROM recurrence_rules r WHERE r.owner_id=v_owner_id AND r.status='ativo' AND p_data>=r.start_date::date AND (r.end_date IS NULL OR p_data<=r.end_date::date) AND v_db_dow=ANY(r.weekdays) AND r.start_time::time<v_slot_fim AND r.end_time::time>v_slot LOOP
                v_rule_start_dow := extract(dow from v_rule.start_date)::int;
                v_first_occ := v_rule.start_date + (((v_db_dow - v_rule_start_dow + 7) % 7))::int;
                v_day_diff := (p_data - v_first_occ)::int;
                IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
                    v_ex_type := NULL;
                    SELECT ex.type INTO v_ex_type FROM recurrence_exceptions ex WHERE ex.recurrence_id=v_rule.id AND ex.original_date=p_data LIMIT 1;
                    IF v_ex_type IS NULL OR v_ex_type NOT IN ('cancel_session','patient_missed','therapist_canceled','holiday_block','reschedule_session') THEN v_ocupado := true; EXIT; END IF;
                END IF;
            END LOOP;
        END IF;
        IF NOT v_ocupado THEN
            SELECT EXISTS (SELECT 1 FROM recurrence_exceptions ex JOIN recurrence_rules r ON r.id=ex.recurrence_id WHERE r.owner_id=v_owner_id AND r.status='ativo' AND ex.type='reschedule_session' AND ex.new_date=p_data AND COALESCE(ex.new_start_time,r.start_time)::time<v_slot_fim AND COALESCE(ex.new_end_time,r.end_time)::time>v_slot) INTO v_ocupado;
        END IF;
        IF NOT v_ocupado THEN
            -- agendador_solicitacoes FICA em public (F1b)
            SELECT EXISTS (SELECT 1 FROM public.agendador_solicitacoes sol WHERE sol.owner_id=v_owner_id AND sol.status='pendente' AND sol.data_solicitada=p_data AND sol.hora_solicitada=v_slot AND (sol.reservado_ate IS NULL OR sol.reservado_ate>v_agora)) INTO v_ocupado;
        END IF;
        hora := v_slot; disponivel := NOT v_ocupado; RETURN NEXT;
    END LOOP;
END $$;

-- match_patient_by_phone só pra service_role (edge)
REVOKE ALL ON FUNCTION public.match_patient_by_phone(uuid, text) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.match_patient_by_phone(uuid, text) TO service_role;

COMMIT;
