-- PolyWeather minimal commerce/auth schema (P0)
-- Run in Supabase SQL editor.

create extension if not exists pgcrypto;

create table if not exists public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  email text not null default '',
  telegram_user_id bigint,
  telegram_username text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_profiles_email
  on public.profiles(email)
  include (id);

create index if not exists idx_profiles_id_lookup
  on public.profiles(id)
  include (email, created_at);

create table if not exists public.subscriptions (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  plan_code text not null,
  status text not null check (status in ('active', 'paused', 'expired', 'cancelled')),
  starts_at timestamptz not null default now(),
  expires_at timestamptz not null,
  source text not null default 'manual',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_subscriptions_user_status_expiry
  on public.subscriptions(user_id, expires_at desc)
  include (id, starts_at, plan_code, source)
  where status = 'active';

create index if not exists idx_subscriptions_status_expiry
  on public.subscriptions(expires_at asc)
  include (user_id, starts_at, plan_code)
  where status = 'active';

create index if not exists idx_subscriptions_user_created
  on public.subscriptions(user_id, created_at desc)
  include (id, status, plan_code, source, starts_at, expires_at, updated_at);

create unique index if not exists uq_subscriptions_signup_trial_user
  on public.subscriptions(user_id)
  where plan_code = 'signup_trial_3d'
    and source = 'signup_trial';

create table if not exists public.payments (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  amount numeric(18, 6) not null,
  currency text not null default 'USDC',
  chain text not null default 'polygon',
  tx_hash text unique,
  status text not null check (status in ('pending', 'confirmed', 'failed', 'refunded')),
  raw_payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_payments_created_at
  on public.payments(created_at desc)
  include (id, user_id, amount, currency, chain, tx_hash, status);

create table if not exists public.entitlement_events (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  action text not null,
  reason text not null default '',
  actor text not null default 'system',
  payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

create table if not exists public.user_wallets (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  chain_id integer not null default 137,
  address text not null,
  status text not null default 'active' check (status in ('active', 'revoked')),
  is_primary boolean not null default false,
  verified_at timestamptz,
  last_used_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique(chain_id, address)
);

create index if not exists idx_user_wallets_user_chain
  on public.user_wallets(user_id, chain_id, is_primary desc, verified_at desc)
  include (id, address)
  where status = 'active';

create index if not exists idx_user_wallets_chain_address_owner
  on public.user_wallets(chain_id, address)
  include (user_id, status);

create table if not exists public.trial_claims (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  email text not null default '',
  telegram_user_id bigint,
  primary_wallet_address text,
  metadata jsonb not null default '{}'::jsonb,
  claimed_at timestamptz not null default now(),
  created_at timestamptz not null default now()
);

create unique index if not exists uq_trial_claims_user
  on public.trial_claims(user_id);

create unique index if not exists uq_trial_claims_email
  on public.trial_claims(lower(email))
  where email <> '';

create unique index if not exists uq_trial_claims_telegram
  on public.trial_claims(telegram_user_id)
  where telegram_user_id is not null;

create table if not exists public.trial_claim_wallets (
  id bigserial primary key,
  trial_claim_id bigint not null references public.trial_claims(id) on delete cascade,
  wallet_address text not null,
  created_at timestamptz not null default now()
);

create unique index if not exists uq_trial_claim_wallets_address
  on public.trial_claim_wallets(lower(wallet_address));

create or replace function public.claim_signup_trial(
  p_user_id uuid,
  p_email text default '',
  p_telegram_user_id bigint default null,
  p_wallet_addresses text[] default array[]::text[]
)
returns jsonb
language plpgsql
security definer
set search_path = public, auth
as $$
declare
  v_email text := lower(trim(coalesce(p_email, '')));
  v_wallets text[] := (
    select coalesce(array_agg(distinct lower(trim(input_wallet.value))), array[]::text[])
    from unnest(coalesce(p_wallet_addresses, array[]::text[])) as input_wallet(value)
    where trim(input_wallet.value) <> ''
  );
  v_now timestamptz := now();
  v_expires timestamptz := v_now + interval '3 days';
  v_claim public.trial_claims%rowtype;
  v_claim_id bigint;
  v_rows integer := 0;
begin
  if p_user_id is null then
    return jsonb_build_object('created', false, 'reason', 'missing_user_id');
  end if;

  select tc.*
    into v_claim
  from public.trial_claims tc
  where tc.user_id = p_user_id
     or (v_email <> '' and lower(tc.email) = v_email)
     or (p_telegram_user_id is not null and tc.telegram_user_id = p_telegram_user_id)
     or exists (
        select 1
        from public.trial_claim_wallets tcw
        where tcw.trial_claim_id = tc.id
          and lower(tcw.wallet_address) = any(v_wallets)
     )
  order by
    case when tc.user_id = p_user_id then 0 else 1 end,
    tc.created_at asc,
    tc.id asc
  limit 1;

  if found then
    if v_claim.user_id <> p_user_id then
      return jsonb_build_object('created', false, 'reason', 'already_claimed');
    end if;

    insert into public.subscriptions (
      user_id,
      plan_code,
      status,
      starts_at,
      expires_at,
      source,
      created_at,
      updated_at
    )
    values (
      p_user_id,
      'signup_trial_3d',
      'active',
      coalesce(v_claim.claimed_at, v_now),
      coalesce(v_claim.claimed_at, v_now) + interval '3 days',
      'signup_trial',
      v_now,
      v_now
    )
    on conflict (user_id)
      where plan_code = 'signup_trial_3d'
        and source = 'signup_trial'
      do nothing;
    get diagnostics v_rows = row_count;

    if v_rows > 0 then
      insert into public.entitlement_events (
        user_id,
        action,
        reason,
        actor,
        payload,
        created_at
      )
      values (
        p_user_id,
        'signup_trial_granted',
        'claim_repaired',
        'supabase_auth',
        jsonb_build_object(
          'plan_code', 'signup_trial_3d',
          'expires_at', coalesce(v_claim.claimed_at, v_now) + interval '3 days'
        ),
        v_now
      );
      return jsonb_build_object(
        'created', true,
        'repaired', true,
        'plan_code', 'signup_trial_3d',
        'expires_at', coalesce(v_claim.claimed_at, v_now) + interval '3 days'
      );
    end if;

    return jsonb_build_object('created', false, 'reason', 'already_claimed');
  end if;

  insert into public.trial_claims (
    user_id,
    email,
    telegram_user_id,
    primary_wallet_address,
    metadata,
    claimed_at,
    created_at
  )
  values (
    p_user_id,
    v_email,
    p_telegram_user_id,
    nullif(v_wallets[1], ''),
    jsonb_build_object('wallet_addresses', v_wallets),
    v_now,
    v_now
  )
  returning id into v_claim_id;

  insert into public.trial_claim_wallets (trial_claim_id, wallet_address, created_at)
  select v_claim_id, wallet.value, v_now
  from unnest(v_wallets) as wallet(value)
  on conflict do nothing;

  insert into public.subscriptions (
    user_id,
    plan_code,
    status,
    starts_at,
    expires_at,
    source,
    created_at,
    updated_at
  )
  values (
    p_user_id,
    'signup_trial_3d',
    'active',
    v_now,
    v_expires,
    'signup_trial',
    v_now,
    v_now
  )
  on conflict (user_id)
    where plan_code = 'signup_trial_3d'
      and source = 'signup_trial'
    do nothing;
  get diagnostics v_rows = row_count;

  insert into public.entitlement_events (
    user_id,
    action,
    reason,
    actor,
    payload,
    created_at
  )
  values
    (
      p_user_id,
      'signup_trial_claimed',
      'trial_dedupe',
      'supabase_auth',
      jsonb_build_object(
        'user_id', p_user_id,
        'email', v_email,
        'telegram_user_id', p_telegram_user_id,
        'wallet_addresses', v_wallets,
        'claimed_at', v_now,
        'storage', 'trial_claims'
      ),
      v_now
    ),
    (
      p_user_id,
      'signup_trial_granted',
      'first_auth',
      'supabase_auth',
      jsonb_build_object(
        'plan_code', 'signup_trial_3d',
        'expires_at', v_expires
      ),
      v_now
    );

  if v_rows = 0 then
    return jsonb_build_object('created', false, 'reason', 'already_claimed');
  end if;

  return jsonb_build_object(
    'created', true,
    'plan_code', 'signup_trial_3d',
    'expires_at', v_expires
  );
exception
  when unique_violation then
    return jsonb_build_object('created', false, 'reason', 'already_claimed');
end;
$$;

revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from public;
revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from anon;
revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from authenticated;
grant execute on function public.claim_signup_trial(uuid, text, bigint, text[]) to service_role;

create table if not exists public.referral_codes (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  code text not null,
  status text not null default 'active' check (status in ('active', 'revoked')),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create unique index if not exists uq_referral_codes_user
  on public.referral_codes(user_id);

create unique index if not exists uq_referral_codes_code
  on public.referral_codes(upper(code));

create table if not exists public.referral_attributions (
  id bigserial primary key,
  referrer_user_id uuid not null references auth.users(id) on delete cascade,
  referred_user_id uuid not null references auth.users(id) on delete cascade,
  code text not null,
  status text not null default 'pending' check (status in ('pending', 'converted', 'capped', 'cancelled')),
  converted_payment_intent_id text,
  converted_tx_hash text,
  converted_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  check (referrer_user_id <> referred_user_id)
);

create unique index if not exists uq_referral_attributions_referred
  on public.referral_attributions(referred_user_id);

create index if not exists idx_referral_attributions_pending
  on public.referral_attributions(referred_user_id, created_at desc)
  include (id, code, referrer_user_id)
  where status = 'pending';

create table if not exists public.referral_rewards (
  id bigserial primary key,
  referral_attribution_id bigint not null references public.referral_attributions(id) on delete cascade,
  referrer_user_id uuid not null references auth.users(id) on delete cascade,
  referred_user_id uuid not null references auth.users(id) on delete cascade,
  payment_intent_id text not null,
  tx_hash text,
  reward_days integer not null default 0 check (reward_days >= 0 and reward_days <= 30),
  reward_points integer not null default 0 check (reward_points >= 0 and reward_points <= 100000),
  created_at timestamptz not null default now()
);

create unique index if not exists uq_referral_rewards_attribution
  on public.referral_rewards(referral_attribution_id);

create index if not exists idx_referral_rewards_referrer_month
  on public.referral_rewards(referrer_user_id, created_at desc)
  include (id, reward_days, reward_points);

create table if not exists public.points_ledger (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  delta integer not null check (delta <> 0),
  source text not null,
  reason text not null,
  payment_intent_id text,
  referral_attribution_id bigint references public.referral_attributions(id) on delete set null,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

create index if not exists idx_points_ledger_user_created
  on public.points_ledger(user_id, created_at desc);

create index if not exists idx_points_ledger_referral
  on public.points_ledger(referral_attribution_id)
  where referral_attribution_id is not null;

alter table public.points_ledger enable row level security;

drop policy if exists points_ledger_select_own on public.points_ledger;
create policy points_ledger_select_own
  on public.points_ledger
  for select
  to authenticated
  using (auth.uid() = user_id);

create table if not exists public.wallet_link_challenges (
  id bigserial primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  chain_id integer not null default 137,
  address text not null,
  nonce text not null unique,
  message text not null,
  expires_at timestamptz not null,
  consumed_at timestamptz,
  created_at timestamptz not null default now()
);

create table if not exists public.payment_intents (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  plan_code text not null,
  plan_id bigint not null,
  chain_id integer not null default 137,
  token_address text not null,
  receiver_address text not null,
  amount_units numeric(78,0) not null,
  payment_mode text not null default 'strict' check (payment_mode in ('strict', 'flex', 'direct')),
  allowed_wallet text,
  order_id_hex text not null unique,
  status text not null default 'created' check (status in ('created', 'submitted', 'confirmed', 'expired', 'failed', 'cancelled')),
  tx_hash text,
  expires_at timestamptz not null,
  confirmed_at timestamptz,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_payment_intents_status_updated
  on public.payment_intents(status, updated_at desc)
  include (user_id)
  where status in ('submitted', 'confirmed');

create index if not exists idx_payment_intents_user_status_updated
  on public.payment_intents(user_id, status, updated_at desc);

create index if not exists idx_payment_intents_submitted_tx_updated
  on public.payment_intents(updated_at asc)
  include (id, user_id, tx_hash, chain_id)
  where status = 'submitted' and tx_hash is not null;

create index if not exists idx_payment_intents_user_created
  on public.payment_intents(user_id, created_at desc);

create index if not exists idx_payment_intents_tx_hash
  on public.payment_intents(tx_hash)
  include (id, user_id)
  where tx_hash is not null;

create table if not exists public.payment_transactions (
  id bigserial primary key,
  intent_id uuid not null references public.payment_intents(id) on delete cascade,
  tx_hash text not null unique,
  chain_id integer not null default 137,
  from_address text,
  to_address text not null,
  block_number bigint,
  payment_method text not null default 'wallet' check (payment_method in ('wallet', 'manual', 'direct')),
  status text not null default 'submitted' check (status in ('submitted', 'confirmed', 'failed', 'duplicate', 'refund_required')),
  raw_receipt jsonb not null default '{}'::jsonb,
  raw_tx jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_payment_transactions_intent
  on public.payment_transactions(intent_id, created_at desc);

create index if not exists idx_payment_transactions_tx_hash_intent
  on public.payment_transactions(tx_hash)
  include (intent_id);

create or replace function public.sync_profile_from_auth()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
  insert into public.profiles (id, email)
  values (new.id, coalesce(new.email, ''))
  on conflict (id) do update
  set email = excluded.email,
      updated_at = now();
  return new;
end;
$$;

drop trigger if exists on_auth_user_created_polyweather on auth.users;
create trigger on_auth_user_created_polyweather
  after insert on auth.users
  for each row execute function public.sync_profile_from_auth();
