-- =============================================================================
-- Migration: 20260418000003_patient_invite_attempts_log
-- Resolve A#24: log de tentativas de submit no cadastro público externo.
-- -----------------------------------------------------------------------------
-- Observação sobre IP: em RPC Postgres chamada via PostgREST o IP real do
-- cliente não chega aqui (só o do connection pooler). Por isso o registro
-- guarda o user_agent enviado pelo cliente (quando disponível) + metadados
-- resolvidos (owner, tenant). Rate-limit real por IP deve ser feito em edge
-- function no futuro (A#20).
-- =============================================================================

CREATE TABLE IF NOT EXISTS public.patient_invite_attempts (
  id             uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
  token          text         NOT NULL,
  ok             boolean      NOT NULL,
  error_code     text,
  error_msg      text,
  client_info    text,           -- user_agent enviado pelo cliente (cap 500 no INSERT)
  owner_id       uuid,           -- resolvido do token quando possível
  tenant_id      uuid,
  created_at     timestamptz  NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_patient_invite_attempts_created ON public.patient_invite_attempts(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_patient_invite_attempts_token ON public.patient_invite_attempts(token);
CREATE INDEX IF NOT EXISTS idx_patient_invite_attempts_owner ON public.patient_invite_attempts(owner_id);
CREATE INDEX IF NOT EXISTS idx_patient_invite_attempts_ok ON public.patient_invite_attempts(ok) WHERE ok = false;

ALTER TABLE public.patient_invite_attempts ENABLE ROW LEVEL SECURITY;

-- Owner vê suas próprias tentativas (qualquer flood/erro que envolveu seus links)
DROP POLICY IF EXISTS patient_invite_attempts_owner_read ON public.patient_invite_attempts;
CREATE POLICY patient_invite_attempts_owner_read
ON public.patient_invite_attempts FOR SELECT
TO authenticated
USING (owner_id = auth.uid() OR public.is_saas_admin());

COMMENT ON TABLE public.patient_invite_attempts IS
'Log de tentativas (ok e falhas) de submit do form público de cadastro externo. Base para monitoramento de flood/tentativas maliciosas. Sem IP direto — proteção LGPD.';

COMMENT ON COLUMN public.patient_invite_attempts.client_info IS
'User-agent enviado pelo cliente (opcional). Limitado a 500 chars no insert. Não contém PII.';

-- =============================================================================
-- create_patient_intake_request_v2 — versão instrumentada
-- -----------------------------------------------------------------------------
-- Mesma função do hardening anterior, agora com log em patient_invite_attempts.
-- O log é feito num bloco EXCEPTION que NUNCA propaga falha de log pro fluxo
-- principal (log falhar jamais deve impedir o cadastro de ser aceito).
-- =============================================================================
CREATE OR REPLACE FUNCTION public.create_patient_intake_request_v2(
  p_token       text,
  p_payload     jsonb,
  p_client_info text DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
DECLARE
  v_owner_id   uuid;
  v_tenant_id  uuid;
  v_active     boolean;
  v_expires    timestamptz;
  v_max_uses   int;
  v_uses       int;
  v_intake_id  uuid;
  v_birth_raw  text;
  v_birth      date;
  v_email      text;
  v_email_alt  text;
  v_nome       text;
  v_consent    boolean;
  v_genero     text;
  v_estado_civil text;
  v_err_msg    text;
  v_err_code   text;
  v_clean_info text;

  c_generos        text[] := ARRAY['male','female','non_binary','other','na'];
  c_estados_civis  text[] := ARRAY['single','married','divorced','widowed','na'];

  -- Helper para logar: escreve em patient_invite_attempts e não propaga erros.
  -- Implementado inline porque PL/pgSQL não permite sub-rotina local fácil.
BEGIN
  -- Sanitiza client_info recebido (cap + trim)
  v_clean_info := nullif(left(trim(coalesce(p_client_info, '')), 500), '');

  -- ───────────────────────────────────────────────────────────────────────
  -- Resolve invite + valida TUDO (A#16)
  -- ───────────────────────────────────────────────────────────────────────
  SELECT owner_id, tenant_id, active, expires_at, max_uses, uses
    INTO v_owner_id, v_tenant_id, v_active, v_expires, v_max_uses, v_uses
  FROM public.patient_invites
  WHERE token = p_token
  LIMIT 1;

  IF v_owner_id IS NULL THEN
    v_err_code := 'TOKEN_INVALID';
    v_err_msg := 'Token inválido';
    -- Log + raise (owner_id NULL porque token não bateu)
    BEGIN
      INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info)
      VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info);
    EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg USING ERRCODE = '28000';
  END IF;

  IF v_active IS NOT TRUE THEN
    v_err_code := 'TOKEN_DISABLED';
    v_err_msg := 'Link desativado';
    BEGIN
      INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id)
      VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id);
    EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg USING ERRCODE = '28000';
  END IF;

  IF v_expires IS NOT NULL AND now() > v_expires THEN
    v_err_code := 'TOKEN_EXPIRED';
    v_err_msg := 'Link expirado';
    BEGIN
      INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id)
      VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id);
    EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg USING ERRCODE = '28000';
  END IF;

  IF v_max_uses IS NOT NULL AND v_uses >= v_max_uses THEN
    v_err_code := 'TOKEN_MAX_USES';
    v_err_msg := 'Limite de uso atingido';
    BEGIN
      INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id)
      VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id);
    EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg USING ERRCODE = '28000';
  END IF;

  -- Resolve tenant_id se invite não tiver (A#19)
  IF v_tenant_id IS NULL THEN
    SELECT tenant_id
      INTO v_tenant_id
    FROM public.tenant_members
    WHERE user_id = v_owner_id
      AND status = 'active'
    ORDER BY created_at ASC
    LIMIT 1;
  END IF;

  -- ───────────────────────────────────────────────────────────────────────
  -- Sanitização + validações de campos (A#27)
  -- ───────────────────────────────────────────────────────────────────────
  v_nome := nullif(trim(p_payload->>'nome_completo'), '');
  IF v_nome IS NULL THEN
    v_err_code := 'VALIDATION'; v_err_msg := 'Nome é obrigatório';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;
  IF length(v_nome) > 200 THEN
    v_err_code := 'VALIDATION'; v_err_msg := 'Nome muito longo';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;

  v_email := nullif(lower(trim(p_payload->>'email_principal')), '');
  IF v_email IS NULL THEN
    v_err_code := 'VALIDATION'; v_err_msg := 'E-mail é obrigatório';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;
  IF length(v_email) > 120 THEN
    v_err_code := 'VALIDATION'; v_err_msg := 'E-mail muito longo';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;
  IF v_email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
    v_err_code := 'VALIDATION'; v_err_msg := 'E-mail inválido';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;

  v_email_alt := nullif(lower(trim(p_payload->>'email_alternativo')), '');
  IF v_email_alt IS NOT NULL THEN
    IF length(v_email_alt) > 120 THEN
      v_err_code := 'VALIDATION'; v_err_msg := 'E-mail alternativo muito longo';
      BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
      RAISE EXCEPTION '%', v_err_msg;
    END IF;
    IF v_email_alt !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
      v_err_code := 'VALIDATION'; v_err_msg := 'E-mail alternativo inválido';
      BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
      RAISE EXCEPTION '%', v_err_msg;
    END IF;
  END IF;

  v_consent := coalesce((p_payload->>'consent')::boolean, false);
  IF v_consent IS NOT TRUE THEN
    v_err_code := 'CONSENT_REQUIRED'; v_err_msg := 'Consentimento é obrigatório';
    BEGIN INSERT INTO public.patient_invite_attempts (token, ok, error_code, error_msg, client_info, owner_id, tenant_id) VALUES (p_token, false, v_err_code, v_err_msg, v_clean_info, v_owner_id, v_tenant_id); EXCEPTION WHEN OTHERS THEN NULL; END;
    RAISE EXCEPTION '%', v_err_msg;
  END IF;

  v_birth_raw := nullif(trim(coalesce(p_payload->>'data_nascimento', '')), '');
  v_birth := CASE
    WHEN v_birth_raw IS NULL THEN NULL
    WHEN v_birth_raw ~ '^\d{4}-\d{2}-\d{2}$' THEN v_birth_raw::date
    WHEN v_birth_raw ~ '^\d{2}-\d{2}-\d{4}$' THEN to_date(v_birth_raw, 'DD-MM-YYYY')
    ELSE NULL
  END;
  IF v_birth IS NOT NULL AND (v_birth > current_date OR v_birth < '1900-01-01'::date) THEN
    v_birth := NULL;
  END IF;

  v_genero := nullif(trim(p_payload->>'genero'), '');
  IF v_genero IS NOT NULL AND NOT (v_genero = ANY(c_generos)) THEN
    v_genero := NULL;
  END IF;

  v_estado_civil := nullif(trim(p_payload->>'estado_civil'), '');
  IF v_estado_civil IS NOT NULL AND NOT (v_estado_civil = ANY(c_estados_civis)) THEN
    v_estado_civil := NULL;
  END IF;

  -- ───────────────────────────────────────────────────────────────────────
  -- INSERT
  -- ───────────────────────────────────────────────────────────────────────
  INSERT INTO public.patient_intake_requests (
    owner_id, tenant_id, token, status, consent,
    nome_completo, email_principal, email_alternativo, telefone, telefone_alternativo,
    avatar_url,
    data_nascimento, cpf, rg, genero, estado_civil,
    profissao, escolaridade, nacionalidade, naturalidade,
    cep, pais, cidade, estado, endereco, numero, complemento, bairro,
    observacoes, encaminhado_por, onde_nos_conheceu
  )
  VALUES (
    v_owner_id, v_tenant_id, p_token, 'new', v_consent,
    v_nome, v_email, v_email_alt,
    nullif(regexp_replace(coalesce(p_payload->>'telefone',''), '\D', '', 'g'), ''),
    nullif(regexp_replace(coalesce(p_payload->>'telefone_alternativo',''), '\D', '', 'g'), ''),
    left(nullif(trim(p_payload->>'avatar_url'), ''), 500),
    v_birth,
    nullif(regexp_replace(coalesce(p_payload->>'cpf',''), '\D', '', 'g'), ''),
    left(nullif(trim(p_payload->>'rg'), ''), 20),
    v_genero, v_estado_civil,
    left(nullif(trim(p_payload->>'profissao'), ''), 120),
    left(nullif(trim(p_payload->>'escolaridade'), ''), 120),
    left(nullif(trim(p_payload->>'nacionalidade'), ''), 80),
    left(nullif(trim(p_payload->>'naturalidade'), ''), 120),
    nullif(regexp_replace(coalesce(p_payload->>'cep',''), '\D', '', 'g'), ''),
    left(nullif(trim(p_payload->>'pais'), ''), 60),
    left(nullif(trim(p_payload->>'cidade'), ''), 120),
    left(nullif(trim(p_payload->>'estado'), ''), 2),
    left(nullif(trim(p_payload->>'endereco'), ''), 200),
    left(nullif(trim(p_payload->>'numero'), ''), 20),
    left(nullif(trim(p_payload->>'complemento'), ''), 120),
    left(nullif(trim(p_payload->>'bairro'), ''), 120),
    left(nullif(trim(p_payload->>'observacoes'), ''), 2000),
    left(nullif(trim(p_payload->>'encaminhado_por'), ''), 120),
    left(nullif(trim(p_payload->>'onde_nos_conheceu'), ''), 80)
  )
  RETURNING id INTO v_intake_id;

  UPDATE public.patient_invites
    SET uses = uses + 1
  WHERE token = p_token;

  -- Log de sucesso (best-effort, não propaga erro)
  BEGIN
    INSERT INTO public.patient_invite_attempts (token, ok, client_info, owner_id, tenant_id)
    VALUES (p_token, true, v_clean_info, v_owner_id, v_tenant_id);
  EXCEPTION WHEN OTHERS THEN NULL; END;

  RETURN v_intake_id;
END;
$function$;

COMMENT ON FUNCTION public.create_patient_intake_request_v2(text, jsonb, text) IS
'Hardened 2026-04-18: valida active/expires/max_uses + incrementa uses; sanitiza todos os campos (trim, length, regex); resolve tenant_id; rejeita notas_internas; exige consent=true; registra cada tentativa em patient_invite_attempts (A#24).';
