-- =============================================================================
-- F6.2 Lote B — triggers schema-aware
-- ⚠️ APLICAR COMO supabase_admin (trigger functions sao owned por supabase_admin):
--   docker exec -i -e PGPASSWORD=postgres supabase_db_agenciapsi-primesakai \n--     psql -U supabase_admin -h 127.0.0.1 -d postgres -v ON_ERROR_STOP=1 \n--     < database-novo/manual/f6_2b_schema_aware_triggers.supabase_admin.sql
--
-- Estratégia hybrid: as funções são reescritas IN PLACE pra operar no schema do
-- TG_TABLE_SCHEMA (search_path dinâmico + tenant_id_for_schema). Como ficariam
-- erradas nas tabelas de public (TG_TABLE_SCHEMA='public'), DESANEXAMOS dos
-- tenant-tables de public e ANEXAMOS só nos schemas. Writes de public via RPCs
-- ainda-não-migrados (Lote D) perdem esses side-effects no curto hybrid —
-- aceitável (public vai ser dropado na F6.3 e o app lê dos schemas).
--
-- Exclui os que escrevem em notifications (Lote C, com o split):
--   notify_on_session_status, fanout_inbound_message_to_notifications,
--   cancel_notifications_on_opt_out/on_session_cancel, fn_notify_agenda_status_change
-- =============================================================================

BEGIN;

-- ───────────────────────────────────────────────────────────────────────────
-- 1) Rewrites — tabelas tenant via search_path (unqualified); globais com public.
-- ───────────────────────────────────────────────────────────────────────────

-- audit_logs é GLOBAL → tenant_id vem do schema
CREATE OR REPLACE FUNCTION public.log_audit_change()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
    v_tenant_id uuid; v_entity_id text; v_old jsonb; v_new jsonb; v_changed text[];
    v_heavy text[] := ARRAY['content','content_html','content_json','raw_data','signature_data','pdf_blob','binary','body_html','body_text'];
    v_noise text[] := ARRAY['updated_at','last_seen_at','last_activity_at'];
BEGIN
    v_tenant_id := public.tenant_id_for_schema(TG_TABLE_SCHEMA);
    IF TG_OP = 'DELETE' THEN
        v_entity_id := OLD.id::text; v_old := to_jsonb(OLD) - v_heavy; v_new := NULL;
    ELSIF TG_OP = 'INSERT' THEN
        v_entity_id := NEW.id::text; v_old := NULL; v_new := to_jsonb(NEW) - v_heavy;
    ELSE
        v_entity_id := NEW.id::text; v_old := to_jsonb(OLD) - v_heavy; v_new := to_jsonb(NEW) - v_heavy;
        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;
        IF v_changed IS NULL THEN RETURN NEW; END IF;
        IF v_changed <@ v_noise 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 $$;

CREATE OR REPLACE FUNCTION public.trg_fn_patient_status_history()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF (TG_OP = 'INSERT') OR (OLD.status IS DISTINCT FROM NEW.status) THEN
        INSERT INTO patient_status_history (patient_id, status_anterior, status_novo, motivo, encaminhado_para, data_saida, alterado_por, alterado_em)
        VALUES (NEW.id, CASE WHEN TG_OP='INSERT' THEN NULL ELSE OLD.status END, NEW.status, NEW.motivo_saida, NEW.encaminhado_para, NEW.data_saida, auth.uid(), now());
    END IF;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.trg_fn_patient_status_timeline()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF (TG_OP = 'INSERT') OR (OLD.status IS DISTINCT FROM NEW.status) THEN
        INSERT INTO patient_timeline (patient_id, evento_tipo, titulo, descricao, icone_cor, gerado_por, ocorrido_em)
        VALUES (NEW.id, 'status_alterado', 'Status alterado para ' || NEW.status,
            CASE WHEN TG_OP='INSERT' THEN 'Paciente cadastrado'
                 ELSE 'De ' || OLD.status || ' → ' || NEW.status || CASE WHEN NEW.motivo_saida IS NOT NULL THEN ' · ' || NEW.motivo_saida ELSE '' END END,
            CASE NEW.status WHEN 'Ativo' THEN 'green' WHEN 'Alta' THEN 'blue' WHEN 'Inativo' THEN 'gray' WHEN 'Encaminhado' THEN 'amber' WHEN 'Arquivado' THEN 'gray' ELSE 'gray' END,
            auth.uid(), now());
    END IF;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.trg_fn_patient_risco_timeline()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF OLD.risco_elevado IS DISTINCT FROM NEW.risco_elevado THEN
        INSERT INTO patient_timeline (patient_id, evento_tipo, titulo, descricao, icone_cor, gerado_por, ocorrido_em)
        VALUES (NEW.id, CASE WHEN NEW.risco_elevado THEN 'risco_sinalizado' ELSE 'risco_removido' END,
            CASE WHEN NEW.risco_elevado THEN 'Risco elevado sinalizado' ELSE 'Sinalização de risco removida' END,
            NEW.risco_nota, CASE WHEN NEW.risco_elevado THEN 'red' ELSE 'green' END, auth.uid(), now());
    END IF;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.auto_create_financial_record_from_session()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
    v_price numeric(10,2); v_services_total numeric(10,2); v_already_billed boolean;
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF NEW.status::text <> 'realizado' THEN RETURN NEW; END IF;
    IF OLD.status IS NOT DISTINCT FROM NEW.status THEN RETURN NEW; END IF;
    IF NEW.tipo::text <> 'sessao' THEN RETURN NEW; END IF;
    IF NEW.patient_id IS NULL THEN RETURN NEW; END IF;
    IF NEW.billing_contract_id IS NOT NULL THEN RETURN NEW; END IF;

    SELECT billed INTO v_already_billed FROM agenda_eventos WHERE id = NEW.id;
    IF v_already_billed = TRUE THEN
        IF EXISTS (SELECT 1 FROM financial_records WHERE agenda_evento_id = NEW.id AND deleted_at IS NULL) THEN
            RETURN NEW;
        END IF;
    END IF;

    v_price := NULL;
    IF NEW.recurrence_id IS NOT NULL THEN
        SELECT COALESCE(SUM(rrs.final_price), 0) INTO v_services_total
        FROM recurrence_rule_services rrs WHERE rrs.rule_id = NEW.recurrence_id;
        IF v_services_total > 0 THEN v_price := v_services_total; END IF;
        IF v_price IS NULL OR v_price = 0 THEN
            SELECT price INTO v_price FROM recurrence_rules WHERE id = NEW.recurrence_id;
        END IF;
    END IF;
    IF v_price IS NULL OR v_price = 0 THEN v_price := NEW.price; END IF;
    IF v_price IS NULL OR v_price <= 0 THEN RETURN NEW; END IF;

    INSERT INTO financial_records (owner_id, patient_id, agenda_evento_id, type, amount, discount_amount, final_amount, clinic_fee_pct, clinic_fee_amount, status, due_date)
    VALUES (NEW.owner_id, NEW.patient_id, NEW.id, 'receita', v_price, 0, v_price, 0, 0, 'pending', (NEW.inicio_em::date + 7));

    UPDATE agenda_eventos SET billed = TRUE WHERE id = NEW.id;
    RETURN NEW;
EXCEPTION WHEN OTHERS THEN
    RAISE WARNING '[auto_create_financial_record_from_session] evento=% erro=%', NEW.id, SQLERRM;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.fn_sla_resolve_on_outbound()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_thread_key text;
BEGIN
    IF NEW.direction <> 'outbound' THEN RETURN NEW; END IF;
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    v_thread_key := COALESCE(NEW.patient_id::text, 'anon:' || COALESCE(NEW.to_number, 'unknown'));
    UPDATE conversation_sla_breaches SET resolved_at = now(), resolved_by_message_id = NEW.id
    WHERE thread_key = v_thread_key AND resolved_at IS NULL;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.fn_clinical_note_version()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE next_version integer; reason text;
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    SELECT COALESCE(MAX(version_number), 0) + 1 INTO next_version FROM clinical_note_versions WHERE note_id = NEW.id;
    IF TG_OP = 'INSERT' THEN reason := 'criacao';
    ELSIF TG_OP = 'UPDATE' THEN
        IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN reason := 'soft_delete';
        ELSIF NEW.deleted_at IS NULL AND OLD.deleted_at IS NOT NULL THEN reason := 'restore';
        ELSE reason := 'edicao'; END IF;
    ELSE reason := 'desconhecido'; END IF;
    INSERT INTO clinical_note_versions (note_id, version_number, title, content_text, content_structured, change_reason, created_at, created_by)
    VALUES (NEW.id, next_version, NEW.title, NEW.content_text, NEW.content_structured, reason, now(), COALESCE(NEW.updated_by, NEW.created_by));
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.fn_document_signature_timeline()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_patient_id uuid; v_doc_nome text;
BEGIN
    IF NEW.status = 'assinado' AND (OLD.status IS NULL OR OLD.status <> 'assinado') THEN
        PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
        SELECT d.patient_id, d.nome_original INTO v_patient_id, v_doc_nome FROM documents d WHERE d.id = NEW.documento_id;
        IF v_patient_id IS NOT NULL THEN
            INSERT INTO patient_timeline (patient_id, evento_tipo, titulo, descricao, icone_cor, link_ref_tipo, link_ref_id, gerado_por, ocorrido_em)
            VALUES (v_patient_id, 'documento_assinado', 'Documento assinado: ' || COALESCE(v_doc_nome, 'documento'),
                'Assinado por ' || COALESCE(NEW.signatario_nome, NEW.signatario_tipo), 'green', 'documento', NEW.documento_id, NEW.signatario_id, NEW.assinado_em);
        END IF;
    END IF;
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.fn_documents_timeline_insert()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    INSERT INTO patient_timeline (patient_id, evento_tipo, titulo, descricao, icone_cor, link_ref_tipo, link_ref_id, gerado_por, ocorrido_em)
    VALUES (NEW.patient_id, 'documento_adicionado', 'Documento adicionado: ' || COALESCE(NEW.nome_original, 'arquivo'),
        'Tipo: ' || COALESCE(NEW.tipo_documento, 'outro'), 'blue', 'documento', NEW.id, NEW.uploaded_by, NEW.uploaded_at);
    RETURN NEW;
END $$;

CREATE OR REPLACE FUNCTION public.sync_legacy_email_fields()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_entity_type text; v_entity_id uuid; v_primary text; v_secondary text;
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF TG_OP = 'DELETE' THEN v_entity_type := OLD.entity_type; v_entity_id := OLD.entity_id;
    ELSE v_entity_type := NEW.entity_type; v_entity_id := NEW.entity_id; END IF;
    SELECT email INTO v_primary FROM contact_emails WHERE entity_type = v_entity_type AND entity_id = v_entity_id ORDER BY is_primary DESC, position ASC, created_at ASC LIMIT 1;
    SELECT email INTO v_secondary FROM contact_emails WHERE entity_type = v_entity_type AND entity_id = v_entity_id AND is_primary = false ORDER BY position ASC, created_at ASC LIMIT 1;
    IF v_entity_type = 'patient' THEN
        UPDATE patients SET email_principal = v_primary, email_alternativo = v_secondary WHERE id = v_entity_id;
    ELSIF v_entity_type = 'medico' THEN
        UPDATE medicos SET email = v_primary WHERE id = v_entity_id;
    END IF;
    IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
END $$;

CREATE OR REPLACE FUNCTION public.sync_legacy_phone_fields()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_entity_type text; v_entity_id uuid; v_primary text; v_secondary text;
BEGIN
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    IF TG_OP = 'DELETE' THEN v_entity_type := OLD.entity_type; v_entity_id := OLD.entity_id;
    ELSE v_entity_type := NEW.entity_type; v_entity_id := NEW.entity_id; END IF;
    SELECT number INTO v_primary FROM contact_phones WHERE entity_type = v_entity_type AND entity_id = v_entity_id ORDER BY is_primary DESC, position ASC, created_at ASC LIMIT 1;
    SELECT number INTO v_secondary FROM contact_phones WHERE entity_type = v_entity_type AND entity_id = v_entity_id AND is_primary = false ORDER BY position ASC, created_at ASC LIMIT 1;
    IF v_entity_type = 'patient' THEN
        UPDATE patients SET telefone = v_primary, telefone_alternativo = v_secondary WHERE id = v_entity_id;
    ELSIF v_entity_type = 'medico' THEN
        UPDATE medicos SET telefone_profissional = v_primary, telefone_pessoal = v_secondary WHERE id = v_entity_id;
    END IF;
    IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
END $$;

CREATE OR REPLACE FUNCTION public.fn_agenda_regras_semanais_no_overlap()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_count int;
BEGIN
    IF new.ativo IS false THEN RETURN new; END IF;
    PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
    SELECT count(*) INTO v_count FROM agenda_regras_semanais r
    WHERE r.owner_id = new.owner_id AND r.dia_semana = new.dia_semana AND r.ativo IS true
      AND (TG_OP = 'INSERT' OR r.id <> new.id)
      AND (new.hora_inicio < r.hora_fim AND new.hora_fim > r.hora_inicio);
    IF v_count > 0 THEN RAISE EXCEPTION 'Janela sobreposta: já existe uma regra ativa nesse intervalo.'; END IF;
    RETURN new;
END $$;

-- valida member consistency: tenant_id vem do schema; tenant_members é GLOBAL
CREATE OR REPLACE FUNCTION public.patients_validate_member_consistency()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE v_tid uuid; v_tenant_responsible uuid; v_tenant_therapist uuid;
BEGIN
    v_tid := public.tenant_id_for_schema(TG_TABLE_SCHEMA);
    SELECT tenant_id INTO v_tenant_responsible FROM public.tenant_members WHERE id = NEW.responsible_member_id;
    IF v_tenant_responsible IS NULL THEN RAISE EXCEPTION 'Responsible member not found'; END IF;
    IF v_tid IS NULL THEN RAISE EXCEPTION 'tenant não resolvido para schema %', TG_TABLE_SCHEMA; END IF;
    IF v_tenant_responsible <> v_tid THEN RAISE EXCEPTION 'Responsible member must belong to the same tenant'; END IF;
    IF NEW.patient_scope = 'therapist' THEN
        IF NEW.therapist_member_id IS NULL THEN RAISE EXCEPTION 'therapist_member_id is required when patient_scope=therapist'; END IF;
        SELECT tenant_id INTO v_tenant_therapist FROM public.tenant_members WHERE id = NEW.therapist_member_id;
        IF v_tenant_therapist IS NULL THEN RAISE EXCEPTION 'Therapist member not found'; END IF;
        IF v_tenant_therapist <> v_tid THEN RAISE EXCEPTION 'Therapist member must belong to the same tenant'; END IF;
    END IF;
    RETURN NEW;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- 2) sync_busy_mirror — CROSS-TENANT: evento pessoal espelha "Ocupado" nas
--    clínicas onde o owner é therapist. Escreve no schema de OUTROS tenants.
-- ───────────────────────────────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.sync_busy_mirror_agenda_eventos()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
    v_source_tenant uuid;
    is_personal boolean;
    should_mirror boolean;
    v_owner uuid;
    v_src_id uuid;
    clinic record;
    v_cschema text;
BEGIN
    -- anti-recursão: espelho não espelha
    IF TG_OP <> 'DELETE' THEN
        IF NEW.mirror_of_event_id IS NOT NULL THEN RETURN NEW; END IF;
        v_owner := NEW.owner_id; v_src_id := NEW.id;
    ELSE
        IF OLD.mirror_of_event_id IS NOT NULL THEN RETURN OLD; END IF;
        v_owner := OLD.owner_id; v_src_id := OLD.id;
    END IF;

    v_source_tenant := public.tenant_id_for_schema(TG_TABLE_SCHEMA);
    is_personal := (v_source_tenant = v_owner);  -- convenção: tenant pessoal tem id = owner
    IF TG_OP = 'DELETE' THEN
        should_mirror := (OLD.visibility_scope IN ('busy_only','private'));
    ELSE
        should_mirror := (NEW.visibility_scope IN ('busy_only','private'));
    END IF;

    IF NOT is_personal THEN
        IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
        RETURN NEW;
    END IF;

    -- DELETE ou não-deve-espelhar: remove espelhos em todas as clínicas do owner
    IF TG_OP = 'DELETE' OR NOT should_mirror THEN
        FOR clinic IN
            SELECT tm.tenant_id FROM public.tenant_members tm
            WHERE tm.user_id = v_owner AND tm.role = 'therapist' AND tm.status = 'active' AND tm.tenant_id <> v_owner
        LOOP
            v_cschema := public.tenant_schema_for(clinic.tenant_id);
            IF v_cschema IS NULL THEN CONTINUE; END IF;
            EXECUTE format('DELETE FROM %I.agenda_eventos WHERE mirror_of_event_id = %L AND mirror_source = %L',
                           v_cschema, v_src_id, 'personal_busy_mirror');
        END LOOP;
        IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
        RETURN NEW;
    END IF;

    -- INSERT/UPDATE com espelho: upsert "Ocupado" em cada clínica do owner
    FOR clinic IN
        SELECT tm.tenant_id FROM public.tenant_members tm
        WHERE tm.user_id = v_owner AND tm.role = 'therapist' AND tm.status = 'active' AND tm.tenant_id <> v_owner
    LOOP
        v_cschema := public.tenant_schema_for(clinic.tenant_id);
        IF v_cschema IS NULL THEN CONTINUE; END IF;
        EXECUTE format(
            'INSERT INTO %I.agenda_eventos (owner_id, terapeuta_id, patient_id, tipo, status, titulo, observacoes, inicio_em, fim_em, mirror_of_event_id, mirror_source, visibility_scope, created_at, updated_at) '
            || 'VALUES ($1,$1,NULL,$2::public.tipo_evento_agenda,$3::public.status_evento_agenda,$4,NULL,$5,$6,$7,$8,$9,now(),now()) '
            || 'ON CONFLICT (mirror_of_event_id) WHERE mirror_of_event_id IS NOT NULL '
            || 'DO UPDATE SET owner_id=EXCLUDED.owner_id, terapeuta_id=EXCLUDED.terapeuta_id, tipo=EXCLUDED.tipo, status=EXCLUDED.status, titulo=EXCLUDED.titulo, observacoes=EXCLUDED.observacoes, inicio_em=EXCLUDED.inicio_em, fim_em=EXCLUDED.fim_em, updated_at=now()',
            v_cschema)
        USING v_owner, 'bloqueio', 'agendado', 'Ocupado', NEW.inicio_em, NEW.fim_em, v_src_id, 'personal_busy_mirror', 'public';
    END LOOP;

    -- remove espelhos de clínicas onde o vínculo therapist active sumiu
    FOR clinic IN
        SELECT ts.tenant_id, ts.schema_name FROM public.tenant_schemas ts
        WHERE NOT EXISTS (
            SELECT 1 FROM public.tenant_members tm
            WHERE tm.user_id = v_owner AND tm.role='therapist' AND tm.status='active' AND tm.tenant_id = ts.tenant_id
        )
    LOOP
        EXECUTE format('DELETE FROM %I.agenda_eventos WHERE mirror_of_event_id = %L AND mirror_source = %L',
                       clinic.schema_name, v_src_id, 'personal_busy_mirror');
    END LOOP;

    RETURN NEW;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- 3) financial_records_inject_tenant — OBSOLETO no schema (sem coluna tenant_id).
--    Mantém em public (legacy) mas NÃO anexa nos schemas.
-- ───────────────────────────────────────────────────────────────────────────

-- ───────────────────────────────────────────────────────────────────────────
-- 4) Detach dos tenant-tables de public + attach nos schemas
-- ───────────────────────────────────────────────────────────────────────────

-- Detacha das tabelas tenant em public os triggers schema-aware (ficariam errados lá)
DO $$
DECLARE
    aware text[] := ARRAY[
        'log_audit_change','trg_fn_patient_status_history','trg_fn_patient_status_timeline',
        'trg_fn_patient_risco_timeline','auto_create_financial_record_from_session',
        'fn_sla_resolve_on_outbound','fn_clinical_note_version','fn_document_signature_timeline',
        'fn_documents_timeline_insert','sync_legacy_email_fields','sync_legacy_phone_fields',
        'fn_agenda_regras_semanais_no_overlap','patients_validate_member_consistency',
        'sync_busy_mirror_agenda_eventos'
    ];
    r record;
BEGIN
    FOR r IN
        SELECT c.relname AS tab, t.tgname
        FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace
        JOIN pg_proc p ON p.oid=t.tgfoid
        WHERE n.nspname='public' AND NOT t.tgisinternal AND p.proname = ANY(aware)
          AND c.relname IN (SELECT table_name FROM information_schema.tables WHERE table_schema='_tenant_template' AND table_type='BASE TABLE')
    LOOP
        EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', r.tgname, r.tab);
    END LOOP;
END $$;

-- Attach nos schemas. Specs derivadas dos triggerdefs REAIS de public, com
-- tenant_id removido de WHEN/UPDATE OF (não existe no schema). __T__ = schema.tabela.
CREATE OR REPLACE FUNCTION public.attach_schema_aware_triggers(p_schema text)
RETURNS int LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
    specs jsonb := jsonb_build_array(
        jsonb_build_object('tab','patients','name','trg_patient_status_history','spec','AFTER INSERT OR UPDATE OF status ON __T__ FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_status_history()'),
        jsonb_build_object('tab','patients','name','trg_patient_status_timeline','spec','AFTER INSERT OR UPDATE OF status ON __T__ FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_status_timeline()'),
        jsonb_build_object('tab','patients','name','trg_patient_risco_timeline','spec','AFTER UPDATE OF risco_elevado ON __T__ FOR EACH ROW EXECUTE FUNCTION public.trg_fn_patient_risco_timeline()'),
        jsonb_build_object('tab','patients','name','trg_audit_patients','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.log_audit_change()'),
        jsonb_build_object('tab','patients','name','trg_patients_validate_members','spec','BEFORE INSERT OR UPDATE OF responsible_member_id, patient_scope, therapist_member_id ON __T__ FOR EACH ROW EXECUTE FUNCTION public.patients_validate_member_consistency()'),
        jsonb_build_object('tab','agenda_eventos','name','trg_audit_agenda_eventos','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.log_audit_change()'),
        jsonb_build_object('tab','agenda_eventos','name','trg_auto_financial_from_session','spec','AFTER UPDATE OF status ON __T__ FOR EACH ROW EXECUTE FUNCTION public.auto_create_financial_record_from_session()'),
        jsonb_build_object('tab','agenda_eventos','name','trg_agenda_eventos_busy_mirror_ins','spec','AFTER INSERT ON __T__ FOR EACH ROW WHEN (new.mirror_of_event_id IS NULL AND new.visibility_scope = ANY (ARRAY[''busy_only''::text, ''private''::text])) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos()'),
        jsonb_build_object('tab','agenda_eventos','name','trg_agenda_eventos_busy_mirror_upd','spec','AFTER UPDATE ON __T__ FOR EACH ROW WHEN (new.mirror_of_event_id IS NULL AND (new.visibility_scope IS DISTINCT FROM old.visibility_scope OR new.inicio_em IS DISTINCT FROM old.inicio_em OR new.fim_em IS DISTINCT FROM old.fim_em OR new.owner_id IS DISTINCT FROM old.owner_id)) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos()'),
        jsonb_build_object('tab','agenda_eventos','name','trg_agenda_eventos_busy_mirror_del','spec','AFTER DELETE ON __T__ FOR EACH ROW WHEN (old.mirror_of_event_id IS NULL) EXECUTE FUNCTION public.sync_busy_mirror_agenda_eventos()'),
        jsonb_build_object('tab','financial_records','name','trg_audit_financial_records','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.log_audit_change()'),
        jsonb_build_object('tab','financial_records','name','trg_financial_records_auto_overdue','spec','BEFORE UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.trg_fn_financial_records_auto_overdue()'),
        jsonb_build_object('tab','documents','name','trg_audit_documents','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.log_audit_change()'),
        jsonb_build_object('tab','documents','name','trg_documents_timeline_insert','spec','AFTER INSERT ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_documents_timeline_insert()'),
        jsonb_build_object('tab','document_signatures','name','trg_ds_timeline','spec','AFTER UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_document_signature_timeline()'),
        jsonb_build_object('tab','clinical_notes','name','trg_clinical_notes_version_insert','spec','AFTER INSERT ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_clinical_note_version()'),
        jsonb_build_object('tab','clinical_notes','name','trg_clinical_notes_version_update','spec','AFTER UPDATE OF content_text, content_structured, title, deleted_at ON __T__ FOR EACH ROW WHEN (old.content_text IS DISTINCT FROM new.content_text OR old.content_structured IS DISTINCT FROM new.content_structured OR old.title IS DISTINCT FROM new.title OR old.deleted_at IS DISTINCT FROM new.deleted_at) EXECUTE FUNCTION public.fn_clinical_note_version()'),
        jsonb_build_object('tab','conversation_messages','name','trg_sla_resolve_on_outbound','spec','AFTER INSERT ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_sla_resolve_on_outbound()'),
        jsonb_build_object('tab','contact_emails','name','trg_contact_emails_sync_legacy','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.sync_legacy_email_fields()'),
        jsonb_build_object('tab','contact_phones','name','trg_contact_phones_sync_legacy','spec','AFTER INSERT OR DELETE OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.sync_legacy_phone_fields()'),
        jsonb_build_object('tab','agenda_regras_semanais','name','trg_agenda_regras_semanais_no_overlap','spec','BEFORE INSERT OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_agenda_regras_semanais_no_overlap()'),
        jsonb_build_object('tab','agenda_configuracoes','name','trg_agenda_cfg_sync','spec','BEFORE INSERT OR UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.agenda_cfg_sync()')
    );
    el jsonb; v_count int := 0; v_target text;
BEGIN
    IF p_schema NOT LIKE 'tenant\_%' THEN RAISE EXCEPTION 'schema inválido %', p_schema; END IF;
    FOR el IN SELECT * FROM jsonb_array_elements(specs) LOOP
        IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=p_schema AND table_name=(el->>'tab')) THEN CONTINUE; END IF;
        v_target := format('%I.%I', p_schema, el->>'tab');
        EXECUTE format('DROP TRIGGER IF EXISTS %I ON %s', el->>'name', v_target);
        EXECUTE 'CREATE TRIGGER ' || quote_ident(el->>'name') || ' ' || replace(el->>'spec', '__T__', v_target);
        v_count := v_count + 1;
    END LOOP;
    RETURN v_count;
END $$;

DO $$
DECLARE r record; v int;
BEGIN
    FOR r IN SELECT schema_name FROM public.tenant_schemas ORDER BY schema_name LOOP
        v := public.attach_schema_aware_triggers(r.schema_name);
        RAISE NOTICE 'F6.2B %: % triggers schema-aware', r.schema_name, v;
    END LOOP;
END $$;

COMMIT;
