-- 20260729_superadmin_rpc_functions.sql
--
-- Postgres SECURITY DEFINER RPC functions callable from the anon role.
-- The SuperAdmin no longer holds the service-role key — all SuperAdmin
-- writes (push tenant row, push signed license) go through these
-- functions, which run with the function owner's privileges and bypass
-- RLS. The trust boundary moves from "whoever holds the service-role
-- key" to "whoever can present a valid signed license token to
-- upsert_license_cloud".
--
-- Apply in psql against the Supabase project:
--   psql "$SUPABASE_DB_URL" -f sql/20260729_superadmin_rpc_functions.sql
--
-- Idempotent — uses CREATE OR REPLACE FUNCTION.

CREATE OR REPLACE FUNCTION public.upsert_tenant_cloud(p_tenant jsonb)
RETURNS public.tenants
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_row public.tenants;
BEGIN
  INSERT INTO public.tenants AS t (
    id, name, code, contact_email, phone, address, status, updated_at
  )
  VALUES (
    (p_tenant->>'id')::uuid,
    p_tenant->>'name',
    p_tenant->>'code',
    NULLIF(p_tenant->>'contact_email', ''),
    NULLIF(p_tenant->>'phone', ''),
    NULLIF(p_tenant->>'address', ''),
    COALESCE(NULLIF(p_tenant->>'status', ''), 'active'),
    NOW()
  )
  ON CONFLICT (id) DO UPDATE SET
    name = EXCLUDED.name,
    code = EXCLUDED.code,
    contact_email = EXCLUDED.contact_email,
    phone = EXCLUDED.phone,
    address = EXCLUDED.address,
    status = EXCLUDED.status,
    updated_at = NOW()
  RETURNING * INTO v_row;
  RETURN v_row;
END;
$$;

CREATE OR REPLACE FUNCTION public.upsert_license_cloud(p_license jsonb)
RETURNS public.tenant_licenses
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_row public.tenant_licenses;
  v_token text;
  v_payload text;
  v_tid text;
BEGIN
  -- Trust boundary: a license row is only accepted if its
  -- `signed_license_token` field is a well-formed JWT whose payload
  -- `tid` (or `tenant_id`) claim matches the row's `tenant_id`. This
  -- prevents an anon caller from forging a license for an arbitrary
  -- tenant.
  v_token := p_license->>'signed_license_token';
  IF v_token IS NULL OR v_token = '' THEN
    RAISE EXCEPTION 'signed_license_token is required';
  END IF;
  IF (p_license->>'tenant_id') IS NULL THEN
    RAISE EXCEPTION 'tenant_id is required';
  END IF;
  IF split_part(v_token, '.', 2) = '' THEN
    RAISE EXCEPTION 'malformed license token';
  END IF;

  v_payload := convert_from(
    decode(
      replace(replace(split_part(v_token, '.', 2), '-', '+'), '_', '/'),
      'base64'
    ),
    'UTF8'
  );
  v_tid := COALESCE(
    (v_payload::jsonb->>'tid'),
    (v_payload::jsonb->>'tenant_id')
  );
  IF v_tid IS DISTINCT FROM (p_license->>'tenant_id') THEN
    RAISE EXCEPTION 'license token tenant_id mismatch (got %, expected %)',
      v_tid, p_license->>'tenant_id';
  END IF;

  INSERT INTO public.tenant_licenses AS tl (
    id, tenant_id, license_key, signed_license_token, algorithm,
    term_name, academic_year, start_date, end_date,
    grace_period_days, max_students, max_staff,
    status, override_reason, issued_by, updated_at
  )
  VALUES (
    (p_license->>'id')::uuid,
    (p_license->>'tenant_id')::uuid,
    p_license->>'license_key',
    p_license->>'signed_license_token',
    COALESCE(NULLIF(p_license->>'algorithm', ''), 'EdDSA'),
    p_license->>'term_name',
    NULLIF(p_license->>'academic_year', ''),
    (p_license->>'start_date')::date,
    (p_license->>'end_date')::date,
    COALESCE((p_license->>'grace_period_days')::int, 7),
    COALESCE((p_license->>'max_students')::int, 1000),
    COALESCE((p_license->>'max_staff')::int, 200),
    COALESCE(NULLIF(p_license->>'status', ''), 'active'),
    NULLIF(p_license->>'override_reason', ''),
    COALESCE(NULLIF(p_license->>'issued_by', ''), 'SuperAdmin'),
    NOW()
  )
  ON CONFLICT (id) DO UPDATE SET
    license_key = EXCLUDED.license_key,
    signed_license_token = EXCLUDED.signed_license_token,
    algorithm = EXCLUDED.algorithm,
    term_name = EXCLUDED.term_name,
    academic_year = EXCLUDED.academic_year,
    start_date = EXCLUDED.start_date,
    end_date = EXCLUDED.end_date,
    grace_period_days = EXCLUDED.grace_period_days,
    max_students = EXCLUDED.max_students,
    max_staff = EXCLUDED.max_staff,
    status = EXCLUDED.status,
    override_reason = EXCLUDED.override_reason,
    issued_by = EXCLUDED.issued_by,
    updated_at = NOW()
  RETURNING * INTO v_row;
  RETURN v_row;
END;
$$;

-- The anon role needs EXECUTE on the new functions; the function
-- bodies are SECURITY DEFINER so they run as the function owner and
-- bypass RLS for the writes.
GRANT EXECUTE ON FUNCTION public.upsert_tenant_cloud(jsonb) TO anon, authenticated;
GRANT EXECUTE ON FUNCTION public.upsert_license_cloud(jsonb) TO anon, authenticated;
