-- =============================================================================
-- AgenciaPsi — Functions — Agenda
-- =============================================================================
-- agenda_cfg_sync, agendador_dias_disponiveis, agendador_gerar_slug,
-- agendador_slots_disponiveis, cancel_recurrence_from,
-- cancelar_eventos_serie, fn_agenda_regras_semanais_no_overlap,
-- split_recurrence_at, sync_busy_mirror, set_updated_at_recurrence
-- =============================================================================

CREATE FUNCTION public.agenda_cfg_sync() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
begin
  if new.agenda_view_mode = 'custom' then
    new.usar_horario_admin_custom := true;
    new.admin_inicio_visualizacao := new.agenda_custom_start;
    new.admin_fim_visualizacao := new.agenda_custom_end;
  else
    new.usar_horario_admin_custom := false;
  end if;

  return new;
end;
$$;


ALTER FUNCTION public.agenda_cfg_sync() OWNER TO supabase_admin;

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

CREATE 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'
    AS $$
DECLARE
  v_owner_id     uuid;
  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.antecedencia_minima_horas
  INTO v_owner_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_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;

    -- ── Dia inteiro bloqueado? (agenda_bloqueios) ─────────────────────────
    SELECT EXISTS (
      SELECT 1 FROM public.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   -- bloqueio de dia inteiro
        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;

    -- ── Tem slots disponíveis no dia? ─────────────────────────────────────
    SELECT EXISTS (
      SELECT 1 FROM public.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;
$$;


ALTER FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) OWNER TO supabase_admin;

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

CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
  v_slug text;
  v_exists boolean;
BEGIN
  -- só gera se ativou e não tem slug ainda
  IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
    LOOP
      v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
      SELECT EXISTS (
        SELECT 1 FROM public.agendador_configuracoes
        WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
      ) INTO v_exists;
      EXIT WHEN NOT v_exists;
    END LOOP;
    NEW.link_slug := v_slug;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;

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

CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
  v_slug text;
  v_exists boolean;
BEGIN
  -- só gera se ativou e não tem slug ainda
  IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
    LOOP
      v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
      SELECT EXISTS (
        SELECT 1 FROM public.agendador_configuracoes
        WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
      ) INTO v_exists;
      EXIT WHEN NOT v_exists;
    END LOOP;
    NEW.link_slug := v_slug;
  END IF;
  RETURN NEW;
END;
$$;


ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;

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

CREATE 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'
    AS $$
DECLARE
  v_owner_id       uuid;
  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;
  -- loop de recorrências
  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.duracao_sessao_min, c.antecedencia_minima_horas
  INTO v_owner_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_agora  := now();
  v_db_dow := extract(dow from p_data::timestamp)::int;

  -- ── Dia inteiro bloqueado? (agenda_bloqueios sem hora) ───────────────────
  -- Se sim, não há nenhum slot disponível — retorna vazio.
  IF EXISTS (
    SELECT 1 FROM public.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   -- bloqueio de dia inteiro
      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 public.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;

    -- ── Antecedência mínima ──────────────────────────────────────────────────
    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;

    -- ── Bloqueio de horário específico (agenda_bloqueios com hora) ───────────
    IF NOT v_ocupado THEN
      SELECT EXISTS (
        SELECT 1 FROM public.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;

    -- ── Eventos avulsos internos (agenda_eventos) ────────────────────────────
    IF NOT v_ocupado THEN
      SELECT EXISTS (
        SELECT 1 FROM public.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;

    -- ── Recorrências ativas (recurrence_rules) ───────────────────────────────
    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 public.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 public.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;

    -- ── Recorrências remarcadas para este dia ────────────────────────────────
    IF NOT v_ocupado THEN
      SELECT EXISTS (
        SELECT 1
        FROM public.recurrence_exceptions ex
        JOIN public.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;

    -- ── Solicitações públicas pendentes ──────────────────────────────────────
    IF NOT v_ocupado THEN
      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;
$$;


ALTER FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) OWNER TO supabase_admin;

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

CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
BEGIN
  UPDATE public.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;
$$;


ALTER FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;

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

CREATE FUNCTION public.cancel_subscription(p_subscription_id uuid) RETURNS public.subscriptions
CREATE FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone DEFAULT now()) RETURNS integer
    LANGUAGE plpgsql SECURITY DEFINER
    AS $$
DECLARE
  v_count integer;
BEGIN
  UPDATE public.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;
$$;


ALTER FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) OWNER TO supabase_admin;

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

COMMENT ON FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) IS 'Cancela todos os eventos futuros de uma série a partir de p_a_partir_de (inclusive).
   Não cancela eventos já realizados.';


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

CREATE FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) RETURNS public.subscriptions
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.set_updated_at_recurrence() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$;


ALTER FUNCTION public.set_updated_at_recurrence() OWNER TO supabase_admin;

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

CREATE FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) RETURNS uuid
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
DECLARE
  v_old   public.recurrence_rules;
  v_new_id uuid;
BEGIN
  -- busca a regra original
  SELECT * INTO v_old
  FROM public.recurrence_rules
  WHERE id = p_recurrence_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id;
  END IF;

  -- encerra a regra antiga na data anterior
  UPDATE public.recurrence_rules
  SET
    end_date   = p_from_date - INTERVAL '1 day',
    open_ended = false,
    updated_at = now()
  WHERE id = p_recurrence_id;

  -- cria nova regra a partir de p_from_date
  INSERT INTO public.recurrence_rules (
    tenant_id, 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
    tenant_id, 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 public.recurrence_rules
  WHERE id = p_recurrence_id
  RETURNING id INTO v_new_id;

  RETURN v_new_id;
END;
$$;


ALTER FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;

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

CREATE FUNCTION public.subscription_intents_view_insert() RETURNS trigger
CREATE FUNCTION public.sync_busy_mirror_agenda_eventos() RETURNS trigger
    LANGUAGE plpgsql SECURITY DEFINER
    SET search_path TO 'public'
    AS $$
declare
  clinic_tenant uuid;
  is_personal boolean;
  should_mirror boolean;
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;
  else
    if old.mirror_of_event_id is not null then
      return old;
    end if;
  end if;

  -- Define se é pessoal e se deve espelhar
  if (tg_op = 'DELETE') then
    is_personal := (old.tenant_id = old.owner_id);
    should_mirror := (old.visibility_scope in ('busy_only','private'));
  else
    is_personal := (new.tenant_id = new.owner_id);
    should_mirror := (new.visibility_scope in ('busy_only','private'));
  end if;

  -- Se não é pessoal, não faz nada
  if not is_personal then
    if (tg_op = 'DELETE') then
      return old;
    end if;
    return new;
  end if;

  -- DELETE: remove espelhos existentes
  if (tg_op = 'DELETE') then
    delete from public.agenda_eventos e
    where e.mirror_of_event_id = old.id
      and e.mirror_source = 'personal_busy_mirror';

    return old;
  end if;

  -- INSERT/UPDATE:
  -- Se não deve espelhar, remove espelhos e sai
  if not should_mirror then
    delete from public.agenda_eventos e
    where e.mirror_of_event_id = new.id
      and e.mirror_source = 'personal_busy_mirror';

    return new;
  end if;

  -- Para cada clínica onde o usuário é therapist active, cria/atualiza o "Ocupado"
  for clinic_tenant in
    select tm.tenant_id
    from public.tenant_members tm
    where tm.user_id = new.owner_id
      and tm.role = 'therapist'
      and tm.status = 'active'
      and tm.tenant_id <> new.owner_id
  loop
    insert into public.agenda_eventos (
      tenant_id,
      owner_id,
      terapeuta_id,
      paciente_id,
      tipo,
      status,
      titulo,
      observacoes,
      inicio_em,
      fim_em,
      mirror_of_event_id,
      mirror_source,
      visibility_scope,
      created_at,
      updated_at
    ) values (
      clinic_tenant,
      new.owner_id,
      new.owner_id,
      null,
      'bloqueio'::public.tipo_evento_agenda,
      'agendado'::public.status_evento_agenda,
      'Ocupado',
      null,
      new.inicio_em,
      new.fim_em,
      new.id,
      'personal_busy_mirror',
      'public',
      now(),
      now()
    )
    on conflict (tenant_id, 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();
  end loop;

  -- Limpa espelhos de clínicas onde o vínculo therapist active não existe mais
  delete from public.agenda_eventos e
  where e.mirror_of_event_id = new.id
    and e.mirror_source = 'personal_busy_mirror'
    and not exists (
      select 1
      from public.tenant_members tm
      where tm.user_id = new.owner_id
        and tm.role = 'therapist'
        and tm.status = 'active'
        and tm.tenant_id = e.tenant_id
    );

  return new;
end;
$$;


ALTER FUNCTION public.sync_busy_mirror_agenda_eventos() OWNER TO supabase_admin;

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

CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer
