-- =============================================================================
-- AgenciaPsi — Functions — Plans, Subscriptions, Billing
-- =============================================================================
-- activate_subscription_from_intent, cancel_subscription,
-- change_subscription_plan, toggle_plan, transition_subscription,
-- reactivate_subscription, rebuild_owner_entitlements,
-- fix_all_subscription_mismatches, subscription_intents_view_insert,
-- subscriptions_validate_scope, admin_fix_plan_target,
-- set_tenant_feature_exception, guard_no_change_core_plan_key, etc.
-- =============================================================================

CREATE FUNCTION public.activate_subscription_from_intent(p_intent_id uuid) RETURNS public.subscriptions
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_intent record;
  v_sub public.subscriptions;
  v_days int;
  v_user_id uuid;
  v_plan_id uuid;
  v_target text;
begin
  -- lê pela VIEW unificada
  select * into v_intent
  from public.subscription_intents
  where id = p_intent_id;

  if not found then
    raise exception 'Intent não encontrado: %', p_intent_id;
  end if;

  if v_intent.status <> 'paid' then
    raise exception 'Intent precisa estar paid para ativar assinatura';
  end if;

  -- resolve target e plan_id via plans.key
  select p.id, p.target
    into v_plan_id, v_target
  from public.plans p
  where p.key = v_intent.plan_key
  limit 1;

  if v_plan_id is null then
    raise exception 'Plano não encontrado em plans.key = %', v_intent.plan_key;
  end if;

  v_target := lower(coalesce(v_target, ''));

  -- ✅ supervisor adicionado
  if v_target not in ('clinic', 'therapist', 'supervisor') then
    raise exception 'Target inválido em plans.target: %', v_target;
  end if;

  -- regra por target
  if v_target = 'clinic' then
    if v_intent.tenant_id is null then
      raise exception 'Intent sem tenant_id';
    end if;
  else
    -- therapist ou supervisor: vinculado ao user
    v_user_id := v_intent.user_id;
    if v_user_id is null then
      v_user_id := v_intent.created_by_user_id;
    end if;
  end if;

  if v_target in ('therapist', 'supervisor') and v_user_id is null then
    raise exception 'Não foi possível determinar user_id para assinatura %.', v_target;
  end if;

  -- cancela assinatura ativa anterior
  if v_target = 'clinic' then
    update public.subscriptions
    set status = 'cancelled',
        cancelled_at = now()
    where tenant_id = v_intent.tenant_id
      and plan_id = v_plan_id
      and status = 'active';
  else
    -- therapist ou supervisor
    update public.subscriptions
    set status = 'cancelled',
        cancelled_at = now()
    where user_id = v_user_id
      and plan_id = v_plan_id
      and status = 'active'
      and tenant_id is null;
  end if;

  -- duração do plano (30 dias para mensal)
  v_days := case
    when lower(coalesce(v_intent.interval, 'month')) = 'year' then 365
    else 30
  end;

  -- cria nova assinatura
  insert into public.subscriptions (
    user_id,
    plan_id,
    status,
    started_at,
    expires_at,
    cancelled_at,
    activated_at,
    tenant_id,
    plan_key,
    interval,
    source,
    created_at,
    updated_at
  )
  values (
    case when v_target = 'clinic' then null else v_user_id end,
    v_plan_id,
    'active',
    now(),
    now() + make_interval(days => v_days),
    null,
    now(),
    case when v_target = 'clinic' then v_intent.tenant_id else null end,
    v_intent.plan_key,
    v_intent.interval,
    'manual',
    now(),
    now()
  )
  returning * into v_sub;

  -- grava vínculo intent → subscription
  if v_target = 'clinic' then
    update public.subscription_intents_tenant
    set subscription_id = v_sub.id
    where id = p_intent_id;
  else
    update public.subscription_intents_personal
    set subscription_id = v_sub.id
    where id = p_intent_id;
  end if;

  return v_sub;
end;
$$;


ALTER FUNCTION public.activate_subscription_from_intent(p_intent_id uuid) OWNER TO supabase_admin;

--
-- Name: admin_credit_addon(uuid, text, integer, uuid, text, text, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.admin_credit_addon(p_tenant_id uuid, p_addon_type text, p_amount integer, p_product_id uuid DEFAULT NULL::uuid, p_description text DEFAULT 'Crédito manual'::text, p_payment_method text DEFAULT 'manual'::text, p_price_cents integer DEFAULT 0) RETURNS jsonb
CREATE FUNCTION public.admin_credit_addon(p_tenant_id uuid, p_addon_type text, p_amount integer, p_product_id uuid DEFAULT NULL::uuid, p_description text DEFAULT 'Crédito manual'::text, p_payment_method text DEFAULT 'manual'::text, p_price_cents integer DEFAULT 0) RETURNS jsonb
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
DECLARE
  v_credit   addon_credits%ROWTYPE;
  v_balance_before INTEGER;
  v_balance_after  INTEGER;
  v_tx_id    UUID;
BEGIN
  -- Upsert addon_credits
  INSERT INTO addon_credits (tenant_id, addon_type, balance, total_purchased)
  VALUES (p_tenant_id, p_addon_type, 0, 0)
  ON CONFLICT (tenant_id, addon_type) DO NOTHING;

  -- Lock e leitura
  SELECT * INTO v_credit
  FROM addon_credits
  WHERE tenant_id = p_tenant_id AND addon_type = p_addon_type
  FOR UPDATE;

  v_balance_before := v_credit.balance;
  v_balance_after  := v_credit.balance + p_amount;

  -- Atualiza saldo
  UPDATE addon_credits
  SET balance         = v_balance_after,
      total_purchased = total_purchased + p_amount,
      low_balance_notified = CASE WHEN v_balance_after > COALESCE(low_balance_threshold, 10) THEN false ELSE low_balance_notified END,
      updated_at      = now()
  WHERE id = v_credit.id;

  -- Registra transação
  INSERT INTO addon_transactions (
    tenant_id, addon_type, type, amount,
    balance_before, balance_after,
    product_id, description,
    admin_user_id, payment_method, price_cents
  ) VALUES (
    p_tenant_id, p_addon_type, 'purchase', p_amount,
    v_balance_before, v_balance_after,
    p_product_id, p_description,
    auth.uid(), p_payment_method, p_price_cents
  )
  RETURNING id INTO v_tx_id;

  RETURN jsonb_build_object(
    'success', true,
    'transaction_id', v_tx_id,
    'balance_before', v_balance_before,
    'balance_after', v_balance_after
  );
END;
$$;


ALTER FUNCTION public.admin_credit_addon(p_tenant_id uuid, p_addon_type text, p_amount integer, p_product_id uuid, p_description text, p_payment_method text, p_price_cents integer) OWNER TO supabase_admin;

--
-- Name: FUNCTION admin_credit_addon(p_tenant_id uuid, p_addon_type text, p_amount integer, p_product_id uuid, p_description text, p_payment_method text, p_price_cents integer); Type: COMMENT; Schema: public; Owner: supabase_admin
--

COMMENT ON FUNCTION public.admin_credit_addon(p_tenant_id uuid, p_addon_type text, p_amount integer, p_product_id uuid, p_description text, p_payment_method text, p_price_cents integer) IS 'Admin adiciona créditos de add-on a um tenant. Cria registro se não existir.';


--
-- Name: admin_delete_email_template_global(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.admin_delete_email_template_global(p_id uuid) RETURNS boolean
CREATE FUNCTION public.admin_delete_email_template_global(p_id uuid) RETURNS boolean
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
BEGIN
  DELETE FROM public.email_templates_global WHERE id = p_id;
  IF NOT FOUND THEN
    RAISE EXCEPTION 'Template com id % não encontrado', p_id;
  END IF;
  RETURN true;
END;
$$;


ALTER FUNCTION public.admin_delete_email_template_global(p_id uuid) OWNER TO supabase_admin;

--
-- Name: admin_fix_plan_target(text, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.admin_fix_plan_target(p_plan_key text, p_new_target text) RETURNS void
CREATE FUNCTION public.admin_fix_plan_target(p_plan_key text, p_new_target text) RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_plan_id uuid;
begin
  -- (opcional) restringe targets válidos
  if p_new_target not in ('clinic','therapist') then
    raise exception 'Target inválido: %', p_new_target using errcode='P0001';
  end if;

  -- trava o plano
  select id into v_plan_id
  from public.plans
  where key = p_plan_key
  for update;

  if v_plan_id is null then
    raise exception 'Plano não encontrado: %', p_plan_key using errcode='P0001';
  end if;

  -- segurança: não mexer se existe subscription
  if exists (select 1 from public.subscriptions s where s.plan_id = v_plan_id) then
    raise exception 'Plano % possui subscriptions. Migração bloqueada.', p_plan_key using errcode='P0001';
  end if;

  -- liga bypass SOMENTE nesta transação
  perform set_config('app.plan_migration_bypass', '1', true);

  update public.plans
  set target = p_new_target
  where id = v_plan_id;

end
$$;


ALTER FUNCTION public.admin_fix_plan_target(p_plan_key text, p_new_target text) OWNER TO supabase_admin;

--
-- Name: admin_upsert_email_template_global(uuid, text, text, text, text, text, text, boolean, jsonb); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.admin_upsert_email_template_global(p_id uuid DEFAULT NULL::uuid, p_key text DEFAULT NULL::text, p_domain text DEFAULT NULL::text, p_channel text DEFAULT 'email'::text, p_subject text DEFAULT NULL::text, p_body_html text DEFAULT NULL::text, p_body_text text DEFAULT NULL::text, p_is_active boolean DEFAULT true, p_variables jsonb DEFAULT '{}'::jsonb) RETURNS jsonb
CREATE FUNCTION public.admin_upsert_email_template_global(p_id uuid DEFAULT NULL::uuid, p_key text DEFAULT NULL::text, p_domain text DEFAULT NULL::text, p_channel text DEFAULT 'email'::text, p_subject text DEFAULT NULL::text, p_body_html text DEFAULT NULL::text, p_body_text text DEFAULT NULL::text, p_is_active boolean DEFAULT true, p_variables jsonb DEFAULT '{}'::jsonb) RETURNS jsonb
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
DECLARE
  v_result jsonb;
  v_id uuid;
BEGIN
  -- UPDATE existente
  IF p_id IS NOT NULL THEN
    UPDATE public.email_templates_global
    SET
      subject   = COALESCE(p_subject, subject),
      body_html = COALESCE(p_body_html, body_html),
      body_text = p_body_text,
      is_active = p_is_active,
      variables = COALESCE(p_variables, variables),
      version   = version + 1
    WHERE id = p_id
    RETURNING to_jsonb(email_templates_global.*) INTO v_result;

    IF v_result IS NULL THEN
      RAISE EXCEPTION 'Template com id % não encontrado', p_id;
    END IF;

    RETURN v_result;
  END IF;

  -- INSERT novo
  IF p_key IS NULL OR p_domain IS NULL OR p_subject IS NULL OR p_body_html IS NULL THEN
    RAISE EXCEPTION 'key, domain, subject e body_html são obrigatórios para novo template';
  END IF;

  INSERT INTO public.email_templates_global (key, domain, channel, subject, body_html, body_text, is_active, variables)
  VALUES (p_key, p_domain, p_channel, p_subject, p_body_html, p_body_text, p_is_active, p_variables)
  RETURNING to_jsonb(email_templates_global.*) INTO v_result;

  RETURN v_result;
END;
$$;


ALTER FUNCTION public.admin_upsert_email_template_global(p_id uuid, p_key text, p_domain text, p_channel text, p_subject text, p_body_html text, p_body_text text, p_is_active boolean, p_variables jsonb) OWNER TO supabase_admin;

--
-- Name: agenda_cfg_sync(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.agenda_cfg_sync() RETURNS trigger
CREATE FUNCTION public.cancel_subscription(p_subscription_id uuid) RETURNS public.subscriptions
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_sub public.subscriptions;
  v_owner_type text;
  v_owner_ref uuid;
begin

  select *
  into v_sub
  from public.subscriptions
  where id = p_subscription_id
  for update;

  if not found then
    raise exception 'Subscription não encontrada';
  end if;

  if v_sub.status = 'canceled' then
    return v_sub;
  end if;

  if v_sub.tenant_id is not null then
    v_owner_type := 'clinic';
    v_owner_ref := v_sub.tenant_id;
  elsif v_sub.user_id is not null then
    v_owner_type := 'therapist';
    v_owner_ref := v_sub.user_id;
  else
    v_owner_type := null;
    v_owner_ref := null;
  end if;

  update public.subscriptions
  set status = 'canceled',
      cancel_at_period_end = false,
      updated_at = now()
  where id = p_subscription_id
  returning * into v_sub;

  insert into public.subscription_events(
    subscription_id,
    owner_id,
    owner_type,
    owner_ref,
    event_type,
    old_plan_id,
    new_plan_id,
    created_by,
    reason,
    source,
    metadata
  )
  values (
    v_sub.id,
    v_owner_ref,
    v_owner_type,
    v_owner_ref,
    'canceled',
    v_sub.plan_id,
    v_sub.plan_id,
    auth.uid(),
    'Cancelamento manual via admin',
    'admin_panel',
    jsonb_build_object('previous_status', 'active')
  );

  if v_owner_ref is not null then
    insert into public.entitlements_invalidation(owner_id, changed_at)
    values (v_owner_ref, now())
    on conflict (owner_id)
    do update set changed_at = excluded.changed_at;
  end if;

  return v_sub;

end;
$$;


ALTER FUNCTION public.cancel_subscription(p_subscription_id uuid) OWNER TO supabase_admin;

--
-- Name: cancelar_eventos_serie(uuid, timestamp with time zone); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone DEFAULT now()) RETURNS integer
CREATE FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) RETURNS public.subscriptions
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_sub public.subscriptions;
  v_old_plan uuid;
  v_new_key text;

  v_owner_type text;
  v_owner_ref uuid;

  v_new_target text;
  v_sub_target text;
begin
  select *
    into v_sub
  from public.subscriptions
  where id = p_subscription_id
  for update;

  if not found then
    raise exception 'Subscription não encontrada';
  end if;

  v_old_plan := v_sub.plan_id;

  if v_old_plan = p_new_plan_id then
    return v_sub;
  end if;

  select key, target
    into v_new_key, v_new_target
  from public.plans
  where id = p_new_plan_id;

  if v_new_key is null then
    raise exception 'Plano não encontrado';
  end if;

  v_new_target := lower(coalesce(v_new_target, ''));

  v_sub_target := case
    when v_sub.tenant_id is not null then 'clinic'
    else 'therapist'
  end;

  if v_new_target <> v_sub_target then
    raise exception 'Plano inválido para este tipo de assinatura. Assinatura é % e o plano é %.',
      v_sub_target, v_new_target
      using errcode = 'P0001';
  end if;

  if v_sub.tenant_id is not null then
    v_owner_type := 'clinic';
    v_owner_ref := v_sub.tenant_id;
  elsif v_sub.user_id is not null then
    v_owner_type := 'therapist';
    v_owner_ref := v_sub.user_id;
  else
    v_owner_type := null;
    v_owner_ref := null;
  end if;

  update public.subscriptions
  set plan_id = p_new_plan_id,
      plan_key = v_new_key,
      updated_at = now()
  where id = p_subscription_id
  returning * into v_sub;

  insert into public.subscription_events(
    subscription_id,
    owner_id,
    owner_type,
    owner_ref,
    event_type,
    old_plan_id,
    new_plan_id,
    created_by,
    reason,
    source,
    metadata
  )
  values (
    v_sub.id,
    v_owner_ref,
    v_owner_type,
    v_owner_ref,
    'plan_changed',
    v_old_plan,
    p_new_plan_id,
    auth.uid(),
    'Plan change via DEV menu',
    'dev_menu',
    jsonb_build_object(
      'previous_plan', v_old_plan,
      'new_plan', p_new_plan_id,
      'new_plan_key', v_new_key,
      'new_plan_target', v_new_target
    )
  );

  if v_owner_ref is not null then
    insert into public.entitlements_invalidation (owner_id, changed_at)
    values (v_owner_ref, now())
    on conflict (owner_id)
    do update set changed_at = excluded.changed_at;
  end if;

  return v_sub;
end;
$$;


ALTER FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) OWNER TO supabase_admin;

--
-- Name: cleanup_notification_queue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.cleanup_notification_queue() RETURNS integer
CREATE FUNCTION public.debit_addon_credit(p_tenant_id uuid, p_addon_type text, p_queue_id uuid DEFAULT NULL::uuid, p_description text DEFAULT 'Consumo'::text) RETURNS jsonb
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
DECLARE
  v_credit         addon_credits%ROWTYPE;
  v_balance_before INTEGER;
  v_balance_after  INTEGER;
BEGIN
  -- Lock e leitura
  SELECT * INTO v_credit
  FROM addon_credits
  WHERE tenant_id = p_tenant_id AND addon_type = p_addon_type AND is_active = true
  FOR UPDATE;

  IF NOT FOUND THEN
    RETURN jsonb_build_object('success', false, 'reason', 'no_credits', 'balance', 0);
  END IF;

  -- Verifica saldo
  IF v_credit.balance <= 0 THEN
    RETURN jsonb_build_object('success', false, 'reason', 'insufficient_balance', 'balance', 0);
  END IF;

  -- Verifica rate limit diário
  IF v_credit.daily_limit IS NOT NULL THEN
    -- Reset se passou do dia
    IF v_credit.daily_reset_at IS NULL OR v_credit.daily_reset_at < date_trunc('day', now()) THEN
      UPDATE addon_credits SET daily_used = 0, daily_reset_at = date_trunc('day', now()) + interval '1 day' WHERE id = v_credit.id;
      v_credit.daily_used := 0;
    END IF;

    IF v_credit.daily_used >= v_credit.daily_limit THEN
      RETURN jsonb_build_object('success', false, 'reason', 'daily_limit_reached', 'balance', v_credit.balance);
    END IF;
  END IF;

  -- Verifica rate limit horário
  IF v_credit.hourly_limit IS NOT NULL THEN
    IF v_credit.hourly_reset_at IS NULL OR v_credit.hourly_reset_at < date_trunc('hour', now()) THEN
      UPDATE addon_credits SET hourly_used = 0, hourly_reset_at = date_trunc('hour', now()) + interval '1 hour' WHERE id = v_credit.id;
      v_credit.hourly_used := 0;
    END IF;

    IF v_credit.hourly_used >= v_credit.hourly_limit THEN
      RETURN jsonb_build_object('success', false, 'reason', 'hourly_limit_reached', 'balance', v_credit.balance);
    END IF;
  END IF;

  -- Verifica expiração
  IF v_credit.expires_at IS NOT NULL AND v_credit.expires_at < now() THEN
    RETURN jsonb_build_object('success', false, 'reason', 'credits_expired', 'balance', v_credit.balance);
  END IF;

  v_balance_before := v_credit.balance;
  v_balance_after  := v_credit.balance - 1;

  -- Debita
  UPDATE addon_credits
  SET balance        = v_balance_after,
      total_consumed = total_consumed + 1,
      daily_used     = COALESCE(daily_used, 0) + 1,
      hourly_used    = COALESCE(hourly_used, 0) + 1,
      updated_at     = now()
  WHERE id = v_credit.id;

  -- Registra transação
  INSERT INTO addon_transactions (
    tenant_id, addon_type, type, amount,
    balance_before, balance_after,
    queue_id, description
  ) VALUES (
    p_tenant_id, p_addon_type, 'consume', -1,
    v_balance_before, v_balance_after,
    p_queue_id, p_description
  );

  RETURN jsonb_build_object(
    'success', true,
    'balance_before', v_balance_before,
    'balance_after', v_balance_after
  );
END;
$$;


ALTER FUNCTION public.debit_addon_credit(p_tenant_id uuid, p_addon_type text, p_queue_id uuid, p_description text) OWNER TO supabase_admin;

--
-- Name: FUNCTION debit_addon_credit(p_tenant_id uuid, p_addon_type text, p_queue_id uuid, p_description text); Type: COMMENT; Schema: public; Owner: supabase_admin
--

COMMENT ON FUNCTION public.debit_addon_credit(p_tenant_id uuid, p_addon_type text, p_queue_id uuid, p_description text) IS 'Debita 1 crédito de add-on. Verifica saldo, rate limits e expiração.';


--
-- Name: delete_commitment_full(uuid, uuid); Type: FUNCTION; Schema: public; Owner: postgres
--

CREATE FUNCTION public.delete_commitment_full(p_tenant_id uuid, p_commitment_id uuid) RETURNS jsonb
CREATE FUNCTION public.fix_all_subscription_mismatches() RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  r record;
begin
  for r in
    select distinct s.user_id as owner_id
    from public.subscriptions s
    where s.status = 'active'
      and s.user_id is not null
  loop
    perform public.rebuild_owner_entitlements(r.owner_id);
  end loop;
end;
$$;


ALTER FUNCTION public.fix_all_subscription_mismatches() OWNER TO supabase_admin;

--
-- Name: fn_agenda_regras_semanais_no_overlap(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
declare
  v_count int;
begin
  if new.ativo is false then
    return new;
  end if;

  select count(*) into v_count
  from public.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;
$$;


ALTER FUNCTION public.fn_agenda_regras_semanais_no_overlap() OWNER TO supabase_admin;

--
-- Name: get_financial_report(uuid, date, date, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
CREATE FUNCTION public.guard_no_change_core_plan_key() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
begin
  if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro')
     and new.key is distinct from old.key then
    raise exception 'Não é permitido alterar a key do plano padrão (%).', old.key
      using errcode = 'P0001';
  end if;

  return new;
end $$;


ALTER FUNCTION public.guard_no_change_core_plan_key() OWNER TO supabase_admin;

--
-- Name: guard_no_change_plan_target(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.guard_no_change_plan_target() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
declare
  v_bypass text;
begin
  -- bypass controlado por sessão/transação:
  -- só passa se app.plan_migration_bypass = '1'
  v_bypass := current_setting('app.plan_migration_bypass', true);

  if v_bypass = '1' then
    return new;
  end if;

  -- comportamento original (bloqueia qualquer mudança)
  if new.target is distinct from old.target then
    raise exception 'Não é permitido alterar target do plano (%) de % para %.',
      old.key, old.target, new.target
      using errcode = 'P0001';
  end if;

  return new;
end
$$;


ALTER FUNCTION public.guard_no_change_plan_target() OWNER TO supabase_admin;

--
-- Name: guard_no_delete_core_plans(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.guard_no_delete_core_plans() RETURNS trigger
CREATE FUNCTION public.guard_no_change_plan_target() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
declare
  v_bypass text;
begin
  -- bypass controlado por sessão/transação:
  -- só passa se app.plan_migration_bypass = '1'
  v_bypass := current_setting('app.plan_migration_bypass', true);

  if v_bypass = '1' then
    return new;
  end if;

  -- comportamento original (bloqueia qualquer mudança)
  if new.target is distinct from old.target then
    raise exception 'Não é permitido alterar target do plano (%) de % para %.',
      old.key, old.target, new.target
      using errcode = 'P0001';
  end if;

  return new;
end
$$;


ALTER FUNCTION public.guard_no_change_plan_target() OWNER TO supabase_admin;

--
-- Name: guard_no_delete_core_plans(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.guard_no_delete_core_plans() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
begin
  if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') then
    raise exception 'Plano padrão (%) não pode ser removido.', old.key
      using errcode = 'P0001';
  end if;

  return old;
end $$;


ALTER FUNCTION public.guard_no_delete_core_plans() OWNER TO supabase_admin;

--
-- Name: guard_patient_cannot_own_tenant(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.guard_patient_cannot_own_tenant() RETURNS trigger
CREATE FUNCTION public.guard_no_delete_core_plans() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
begin
  if old.key in ('clinic_free','clinic_pro','therapist_free','therapist_pro') then
    raise exception 'Plano padrão (%) não pode ser removido.', old.key
      using errcode = 'P0001';
  end if;

  return old;
end $$;


ALTER FUNCTION public.guard_no_delete_core_plans() OWNER TO supabase_admin;

--
-- Name: guard_patient_cannot_own_tenant(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.guard_patient_cannot_own_tenant() RETURNS trigger
CREATE FUNCTION public.reactivate_subscription(p_subscription_id uuid) RETURNS public.subscriptions
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_sub public.subscriptions;
  v_owner_type text;
  v_owner_ref uuid;
begin

  select *
  into v_sub
  from public.subscriptions
  where id = p_subscription_id
  for update;

  if not found then
    raise exception 'Subscription não encontrada';
  end if;

  if v_sub.status = 'active' then
    return v_sub;
  end if;

  if v_sub.tenant_id is not null then
    v_owner_type := 'clinic';
    v_owner_ref := v_sub.tenant_id;
  elsif v_sub.user_id is not null then
    v_owner_type := 'therapist';
    v_owner_ref := v_sub.user_id;
  else
    v_owner_type := null;
    v_owner_ref := null;
  end if;

  update public.subscriptions
  set status = 'active',
      cancel_at_period_end = false,
      updated_at = now()
  where id = p_subscription_id
  returning * into v_sub;

  insert into public.subscription_events(
    subscription_id,
    owner_id,
    owner_type,
    owner_ref,
    event_type,
    old_plan_id,
    new_plan_id,
    created_by,
    reason,
    source,
    metadata
  )
  values (
    v_sub.id,
    v_owner_ref,
    v_owner_type,
    v_owner_ref,
    'reactivated',
    v_sub.plan_id,
    v_sub.plan_id,
    auth.uid(),
    'Reativação manual via admin',
    'admin_panel',
    jsonb_build_object('previous_status', 'canceled')
  );

  if v_owner_ref is not null then
    insert into public.entitlements_invalidation(owner_id, changed_at)
    values (v_owner_ref, now())
    on conflict (owner_id)
    do update set changed_at = excluded.changed_at;
  end if;

  return v_sub;

end;
$$;


ALTER FUNCTION public.reactivate_subscription(p_subscription_id uuid) OWNER TO supabase_admin;

--
-- Name: rebuild_owner_entitlements(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.rebuild_owner_entitlements(p_owner_id uuid) RETURNS void
CREATE FUNCTION public.rebuild_owner_entitlements(p_owner_id uuid) RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_plan_id uuid;
begin
  -- Plano ativo do owner (owner = subscriptions.user_id)
  select s.plan_id
    into v_plan_id
  from public.subscriptions s
  where s.user_id = p_owner_id
    and s.status = 'active'
  order by s.created_at desc
  limit 1;

  -- Sempre zera entitlements do owner (rebuild)
  delete from public.owner_feature_entitlements e
  where e.owner_id = p_owner_id;

  -- Se não tem assinatura ativa, acabou
  if v_plan_id is null then
    return;
  end if;

  -- Recria entitlements esperados pelo plano
  insert into public.owner_feature_entitlements (owner_id, feature_key, sources, limits_list)
  select
    p_owner_id as owner_id,
    f.key as feature_key,
    array['plan'::text] as sources,
    '{}'::jsonb as limits_list
  from public.plan_features pf
  join public.features f on f.id = pf.feature_id
  where pf.plan_id = v_plan_id;

end;
$$;


ALTER FUNCTION public.rebuild_owner_entitlements(p_owner_id uuid) OWNER TO supabase_admin;

--
-- Name: revoke_support_session(text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.revoke_support_session(p_token text) RETURNS boolean
CREATE FUNCTION public.set_tenant_feature_exception(p_tenant_id uuid, p_feature_key text, p_enabled boolean, p_reason text DEFAULT NULL::text) RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
begin
  -- ✅ Só owner ou admin do tenant podem alterar features
  if not exists (
    select 1 from public.tenant_members
    where tenant_id = p_tenant_id
      and user_id = auth.uid()
      and role in ('owner', 'admin')
      and status = 'active'
  ) then
    raise exception 'Acesso negado: apenas owner/admin pode alterar features do tenant.';
  end if;

  insert into public.tenant_features (tenant_id, feature_key, enabled)
  values (p_tenant_id, p_feature_key, p_enabled)
  on conflict (tenant_id, feature_key)
  do update set enabled = excluded.enabled;

  insert into public.tenant_feature_exceptions_log (
    tenant_id, feature_key, enabled, reason, created_by
  ) values (
    p_tenant_id, p_feature_key, p_enabled, p_reason, auth.uid()
  );
end;
$$;


ALTER FUNCTION public.set_tenant_feature_exception(p_tenant_id uuid, p_feature_key text, p_enabled boolean, p_reason text) OWNER TO supabase_admin;

--
-- Name: set_updated_at(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.set_updated_at() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;


ALTER FUNCTION public.set_updated_at() OWNER TO supabase_admin;

--
-- Name: set_updated_at_recurrence(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
CREATE FUNCTION public.subscription_intents_view_insert() RETURNS trigger
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_target text;
  v_plan_id uuid;
begin
  select p.id, p.target into v_plan_id, v_target
  from public.plans p
  where p.key = new.plan_key;

  if v_plan_id is null then
    raise exception 'Plano inválido: plan_key=%', new.plan_key;
  end if;

  if lower(v_target) = 'clinic' then
    if new.tenant_id is null then
      raise exception 'Intenção clinic exige tenant_id.';
    end if;

    insert into public.subscription_intents_tenant (
      id, tenant_id, created_by_user_id, email,
      plan_id, plan_key, interval, amount_cents, currency,
      status, source, notes, created_at, paid_at
    ) values (
      coalesce(new.id, gen_random_uuid()),
      new.tenant_id, new.created_by_user_id, new.email,
      v_plan_id, new.plan_key, coalesce(new.interval,'month'),
      new.amount_cents, coalesce(new.currency,'BRL'),
      coalesce(new.status,'pending'), coalesce(new.source,'manual'),
      new.notes, coalesce(new.created_at, now()), new.paid_at
    );

    new.plan_target := 'clinic';
    return new;
  end if;

  -- therapist ou supervisor → tabela personal
  if lower(v_target) in ('therapist', 'supervisor') then
    insert into public.subscription_intents_personal (
      id, user_id, created_by_user_id, email,
      plan_id, plan_key, interval, amount_cents, currency,
      status, source, notes, created_at, paid_at
    ) values (
      coalesce(new.id, gen_random_uuid()),
      new.user_id, new.created_by_user_id, new.email,
      v_plan_id, new.plan_key, coalesce(new.interval,'month'),
      new.amount_cents, coalesce(new.currency,'BRL'),
      coalesce(new.status,'pending'), coalesce(new.source,'manual'),
      new.notes, coalesce(new.created_at, now()), new.paid_at
    );

    new.plan_target := lower(v_target); -- 'therapist' ou 'supervisor'
    return new;
  end if;

  raise exception 'Target de plano não suportado: %', v_target;
end;
$$;


ALTER FUNCTION public.subscription_intents_view_insert() OWNER TO supabase_admin;

--
-- Name: subscriptions_validate_scope(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.subscriptions_validate_scope() RETURNS trigger
CREATE FUNCTION public.subscriptions_validate_scope() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
  v_target text;
BEGIN
  SELECT lower(p.target) INTO v_target
  FROM public.plans p
  WHERE p.id = NEW.plan_id;

  IF v_target IS NULL THEN
    RAISE EXCEPTION 'Plano inválido (target nulo).';
  END IF;

  IF v_target = 'clinic' THEN
    IF NEW.tenant_id IS NULL THEN
      RAISE EXCEPTION 'Assinatura clinic exige tenant_id.';
    END IF;
    IF NEW.user_id IS NOT NULL THEN
      RAISE EXCEPTION 'Assinatura clinic não pode ter user_id (XOR).';
    END IF;

  ELSIF v_target IN ('therapist', 'supervisor') THEN
    -- supervisor é pessoal como therapist
    IF NEW.tenant_id IS NOT NULL THEN
      RAISE EXCEPTION 'Assinatura % não deve ter tenant_id.', v_target;
    END IF;
    IF NEW.user_id IS NULL THEN
      RAISE EXCEPTION 'Assinatura % exige user_id.', v_target;
    END IF;

  ELSIF v_target = 'patient' THEN
    IF NEW.tenant_id IS NOT NULL THEN
      RAISE EXCEPTION 'Assinatura patient não deve ter tenant_id.';
    END IF;
    IF NEW.user_id IS NULL THEN
      RAISE EXCEPTION 'Assinatura patient exige user_id.';
    END IF;

  ELSE
    RAISE EXCEPTION 'Target de plano inválido: %', v_target;
  END IF;

  RETURN NEW;
END;
$$;


ALTER FUNCTION public.subscriptions_validate_scope() OWNER TO supabase_admin;

--
-- Name: sync_busy_mirror_agenda_eventos(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.sync_busy_mirror_agenda_eventos() RETURNS trigger
CREATE FUNCTION public.toggle_plan(owner uuid) RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  current_key text;
  new_key text;
begin
  select p.key into current_key
  from subscriptions s
  join plans p on p.id = s.plan_id
  where s.owner_id = owner
    and s.status = 'active';

  new_key := case
    when current_key = 'pro' then 'free'
    else 'pro'
  end;

  update subscriptions s
  set plan_id = p.id
  from plans p
  where p.key = new_key
    and s.owner_id = owner
    and s.status = 'active';
end;
$$;


ALTER FUNCTION public.toggle_plan(owner uuid) OWNER TO supabase_admin;

--
-- Name: transition_subscription(uuid, text, text, jsonb); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.transition_subscription(p_subscription_id uuid, p_to_status text, p_reason text DEFAULT NULL::text, p_metadata jsonb DEFAULT NULL::jsonb) RETURNS public.subscriptions
CREATE FUNCTION public.transition_subscription(p_subscription_id uuid, p_to_status text, p_reason text DEFAULT NULL::text, p_metadata jsonb DEFAULT NULL::jsonb) RETURNS public.subscriptions
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
declare
  v_sub public.subscriptions;
  v_uid uuid;
  v_is_allowed boolean := false;
begin
  v_uid := auth.uid();

  select *
    into v_sub
  from public.subscriptions
  where id = p_subscription_id;

  if not found then
    raise exception 'Assinatura não encontrada';
  end if;

  -- =====================================================
  -- 🔐 BLOCO DE AUTORIZAÇÃO
  -- =====================================================

  -- 1) SaaS admin pode tudo
  if is_saas_admin() then
    v_is_allowed := true;
  end if;

  -- 2) Assinatura pessoal (therapist)
  if not v_is_allowed
     and v_sub.tenant_id is null
     and v_sub.user_id = v_uid then
    v_is_allowed := true;
  end if;

  -- 3) Assinatura de clinic (tenant)
  if not v_is_allowed
     and v_sub.tenant_id is not null then

    if exists (
      select 1
      from public.tenant_members tm
      where tm.tenant_id = v_sub.tenant_id
        and tm.user_id = v_uid
        and tm.status = 'active'
        and tm.role = 'tenant_admin'
    ) then
      v_is_allowed := true;
    end if;

  end if;

  if not v_is_allowed then
    raise exception 'Sem permissão para transicionar esta assinatura';
  end if;

  -- =====================================================
  -- 🧠 TRANSIÇÃO
  -- =====================================================

  update public.subscriptions
  set status = p_to_status,
      updated_at = now(),
      cancelled_at = case when p_to_status = 'cancelled' then now() else cancelled_at end,
      suspended_at = case when p_to_status = 'suspended' then now() else suspended_at end,
      past_due_since = case when p_to_status = 'past_due' then now() else past_due_since end,
      expired_at = case when p_to_status = 'expired' then now() else expired_at end,
      activated_at = case when p_to_status = 'active' then now() else activated_at end
  where id = p_subscription_id
  returning * into v_sub;

  -- =====================================================
  -- 🧾 EVENT LOG
  -- =====================================================

  insert into public.subscription_events (
    subscription_id,
    owner_id,
    event_type,
    created_at,
    created_by,
    source,
    reason,
    metadata,
    owner_type,
    owner_ref
  )
  values (
    v_sub.id,
    coalesce(v_sub.tenant_id, v_sub.user_id),
    'status_changed',
    now(),
    v_uid,
    'manual_transition',
    p_reason,
    p_metadata,
    case when v_sub.tenant_id is not null then 'tenant' else 'personal' end,
    coalesce(v_sub.tenant_id, v_sub.user_id)
  );

  return v_sub;
end;
$$;


ALTER FUNCTION public.transition_subscription(p_subscription_id uuid, p_to_status text, p_reason text, p_metadata jsonb) OWNER TO supabase_admin;

--
-- Name: trg_fn_financial_records_auto_overdue(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--

CREATE FUNCTION public.trg_fn_financial_records_auto_overdue() RETURNS trigger
