-- =============================================================================
-- Migration: 20260419000006_layered_bot_defense
-- A#20 (rev2) — Defesa em camadas self-hosted (substitui Turnstile).
--
-- Camadas:
--   1. Honeypot field (no front)        → invisível, sempre ativo
--   2. Rate limit por IP no edge        → submission_rate_limits
--   3. Math captcha CONDICIONAL         → só se IP teve N falhas recentes
--   4. Logging em public_submission_attempts (genérico, não só intake)
--   5. Modo paranoid global             → saas_security_config.captcha_required
--
-- Substitui chamadas Turnstile na edge function submit-patient-intake.
-- =============================================================================

-- ─────────────────────────────────────────────────────────────────────────
-- 1. saas_security_config (singleton)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.saas_security_config (
    id                          boolean PRIMARY KEY DEFAULT true,
    honeypot_enabled            boolean NOT NULL DEFAULT true,
    rate_limit_enabled          boolean NOT NULL DEFAULT true,
    rate_limit_window_min       integer NOT NULL DEFAULT 10,
    rate_limit_max_attempts     integer NOT NULL DEFAULT 5,
    captcha_after_failures      integer NOT NULL DEFAULT 3,
    captcha_required_globally   boolean NOT NULL DEFAULT false,
    block_duration_min          integer NOT NULL DEFAULT 30,
    captcha_required_window_min integer NOT NULL DEFAULT 60,
    updated_at                  timestamptz NOT NULL DEFAULT now(),
    updated_by                  uuid,
    CONSTRAINT saas_security_config_singleton CHECK (id = true)
);

INSERT INTO public.saas_security_config (id) VALUES (true)
ON CONFLICT (id) DO NOTHING;

ALTER TABLE public.saas_security_config ENABLE ROW LEVEL SECURITY;

REVOKE ALL ON public.saas_security_config FROM anon, authenticated;
GRANT  SELECT, UPDATE ON public.saas_security_config TO authenticated;

DROP POLICY IF EXISTS saas_security_config_read ON public.saas_security_config;
CREATE POLICY saas_security_config_read ON public.saas_security_config
  FOR SELECT TO authenticated
  USING (true);  -- qualquer logado pode ler config global (não tem segredo)

DROP POLICY IF EXISTS saas_security_config_write ON public.saas_security_config;
CREATE POLICY saas_security_config_write ON public.saas_security_config
  FOR UPDATE TO authenticated
  USING (public.is_saas_admin())
  WITH CHECK (public.is_saas_admin());

COMMENT ON TABLE public.saas_security_config IS 'Singleton: configuração global de defesa contra bots em endpoints públicos.';

-- ─────────────────────────────────────────────────────────────────────────
-- 2. public_submission_attempts (log genérico)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.public_submission_attempts (
    id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    endpoint      text NOT NULL,
    ip_hash       text,
    success       boolean NOT NULL,
    error_code    text,
    error_msg     text,
    blocked_by    text,           -- 'honeypot' | 'rate_limit' | 'captcha' | 'rpc' | null
    user_agent    text,
    metadata      jsonb,
    created_at    timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_psa_endpoint_created   ON public.public_submission_attempts (endpoint, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_psa_ip_hash_created    ON public.public_submission_attempts (ip_hash, created_at DESC) WHERE ip_hash IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_psa_failed             ON public.public_submission_attempts (created_at DESC) WHERE success = false;

ALTER TABLE public.public_submission_attempts ENABLE ROW LEVEL SECURITY;

REVOKE ALL ON public.public_submission_attempts FROM anon, authenticated;
GRANT  SELECT ON public.public_submission_attempts TO authenticated;

DROP POLICY IF EXISTS psa_read_saas_admin ON public.public_submission_attempts;
CREATE POLICY psa_read_saas_admin ON public.public_submission_attempts
  FOR SELECT TO authenticated
  USING (public.is_saas_admin());

COMMENT ON TABLE public.public_submission_attempts IS 'Log de tentativas em endpoints públicos (intake, signup, agendador). Escrita apenas via RPC SECURITY DEFINER.';

-- ─────────────────────────────────────────────────────────────────────────
-- 3. submission_rate_limits (estado vigente por IP+endpoint)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.submission_rate_limits (
    ip_hash                 text NOT NULL,
    endpoint                text NOT NULL,
    attempt_count           integer NOT NULL DEFAULT 0,
    fail_count              integer NOT NULL DEFAULT 0,
    window_start            timestamptz NOT NULL DEFAULT now(),
    blocked_until           timestamptz,
    requires_captcha_until  timestamptz,
    last_attempt_at         timestamptz NOT NULL DEFAULT now(),
    created_at              timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (ip_hash, endpoint)
);

CREATE INDEX IF NOT EXISTS idx_srl_blocked_until ON public.submission_rate_limits (blocked_until) WHERE blocked_until IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_srl_endpoint      ON public.submission_rate_limits (endpoint, last_attempt_at DESC);

ALTER TABLE public.submission_rate_limits ENABLE ROW LEVEL SECURITY;

REVOKE ALL ON public.submission_rate_limits FROM anon, authenticated;
GRANT  SELECT ON public.submission_rate_limits TO authenticated;

DROP POLICY IF EXISTS srl_read_saas_admin ON public.submission_rate_limits;
CREATE POLICY srl_read_saas_admin ON public.submission_rate_limits
  FOR SELECT TO authenticated
  USING (public.is_saas_admin());

COMMENT ON TABLE public.submission_rate_limits IS 'Estado de rate limit por IP+endpoint. Escrita apenas via RPC. SaaS admin lê pra dashboard.';

-- ─────────────────────────────────────────────────────────────────────────
-- 4. math_challenges (TTL 5min, limpa via cron)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.math_challenges (
    id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    question    text NOT NULL,
    answer      integer NOT NULL,
    used        boolean NOT NULL DEFAULT false,
    created_at  timestamptz NOT NULL DEFAULT now(),
    expires_at  timestamptz NOT NULL DEFAULT (now() + interval '5 minutes')
);

CREATE INDEX IF NOT EXISTS idx_mc_expires ON public.math_challenges (expires_at);

ALTER TABLE public.math_challenges ENABLE ROW LEVEL SECURITY;

REVOKE ALL ON public.math_challenges FROM anon, authenticated;
-- nenhum grant: tabela acessada apenas via RPC SECURITY DEFINER

COMMENT ON TABLE public.math_challenges IS 'Challenges de math captcha. TTL 5min. Escrita/leitura apenas via RPC.';
