create extension if not exists pgcrypto;

create table if not exists public.wordle_words (
  position integer primary key,
  word text not null unique check (word ~ '^[a-z]{5}$')
);

create table if not exists public.wordle_dictionary (
  word text primary key check (word ~ '^[a-z]{5}$')
);

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

alter table public.profiles
  add column if not exists username text;

create unique index if not exists profiles_username_key
  on public.profiles (lower(username))
  where username is not null;

create table if not exists public.wordle_rounds (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  word text not null check (word ~ '^[a-z]{5}$'),
  hour_start timestamptz not null,
  started_at timestamptz not null default now(),
  next_playable_at timestamptz not null,
  completed_at timestamptz,
  won boolean,
  guess_count integer check (guess_count between 1 and 6),
  created_at timestamptz not null default now()
);

alter table public.wordle_rounds
  add column if not exists hour_start timestamptz,
  add column if not exists next_playable_at timestamptz;

update public.wordle_rounds
set hour_start = date_trunc('hour', started_at)
where hour_start is null;

update public.wordle_rounds
set next_playable_at = hour_start + interval '1 hour'
where next_playable_at is null;

alter table public.wordle_rounds
  alter column hour_start set not null,
  alter column next_playable_at set not null;

create table if not exists public.wordle_guesses (
  id uuid primary key default gen_random_uuid(),
  round_id uuid not null references public.wordle_rounds(id) on delete cascade,
  user_id uuid not null references auth.users(id) on delete cascade,
  guess text not null check (guess ~ '^[a-z]{5}$'),
  row_index integer not null check (row_index between 1 and 6),
  states jsonb not null,
  created_at timestamptz not null default now(),
  unique (round_id, row_index)
);

create index if not exists wordle_rounds_user_started_idx
  on public.wordle_rounds (user_id, started_at desc);

create unique index if not exists wordle_rounds_user_hour_idx
  on public.wordle_rounds (user_id, hour_start);

create index if not exists wordle_guesses_round_row_idx
  on public.wordle_guesses (round_id, row_index);

alter table public.wordle_words enable row level security;
alter table public.wordle_dictionary enable row level security;
alter table public.profiles enable row level security;
alter table public.wordle_rounds enable row level security;
alter table public.wordle_guesses enable row level security;

drop policy if exists "Users can read their own profile" on public.profiles;
create policy "Users can read their own profile"
  on public.profiles
  for select
  using (auth.uid() = id);

drop policy if exists "Users can update their own profile" on public.profiles;
create policy "Users can update their own profile"
  on public.profiles
  for update
  using (auth.uid() = id)
  with check (auth.uid() = id);

drop policy if exists "Users can read their own rounds" on public.wordle_rounds;
create policy "Users can read their own rounds"
  on public.wordle_rounds
  for select
  using (auth.uid() = user_id);

drop policy if exists "Users can read their own guesses" on public.wordle_guesses;
create policy "Users can read their own guesses"
  on public.wordle_guesses
  for select
  using (auth.uid() = user_id);

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
  insert into public.profiles (id, username, display_name)
  values (
    new.id,
    nullif(lower(regexp_replace(coalesce(new.raw_user_meta_data->>'username', split_part(new.email, '@', 1)), '[^a-z0-9_]', '', 'g')), ''),
    coalesce(new.raw_user_meta_data->>'display_name', new.raw_user_meta_data->>'username', split_part(new.email, '@', 1))
  )
  on conflict (id) do nothing;

  return new;
end;
$$;

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

create or replace function public.score_guess(guess text, answer text)
returns jsonb
language plpgsql
immutable
as $$
declare
  states text[] := array['wrong', 'wrong', 'wrong', 'wrong', 'wrong'];
  remaining_letters text := '';
  guess_letter text;
  answer_letter text;
  match_index integer;
  index integer;
begin
  for index in 1..5 loop
    guess_letter := substr(guess, index, 1);
    answer_letter := substr(answer, index, 1);

    if guess_letter = answer_letter then
      states[index] := 'correct';
    else
      remaining_letters := remaining_letters || answer_letter;
    end if;
  end loop;

  for index in 1..5 loop
    if states[index] = 'correct' then
      continue;
    end if;

    guess_letter := substr(guess, index, 1);
    match_index := strpos(remaining_letters, guess_letter);

    if match_index > 0 then
      states[index] := 'wrong-position';
      remaining_letters := overlay(remaining_letters placing '' from match_index for 1);
    end if;
  end loop;

  return to_jsonb(states);
end;
$$;

create or replace function public.get_hourly_word(hour_start timestamptz)
returns text
language plpgsql
stable
security definer
set search_path = public
as $$
declare
  word_count integer;
  word_offset integer;
  hourly_word text;
begin
  select count(*) into word_count from public.wordle_words;

  if word_count = 0 then
    raise exception 'No hourly words are configured';
  end if;

  word_offset := (floor(extract(epoch from hour_start) / 3600)::bigint % word_count)::integer;

  select public.wordle_words.word
    into hourly_word
  from public.wordle_words
  order by position
  limit 1 offset word_offset;

  return hourly_word;
end;
$$;

create or replace function public.require_confirmed_email()
returns void
language plpgsql
security definer
set search_path = public, auth
as $$
begin
  if auth.uid() is null then
    raise exception 'Authentication required';
  end if;

  if not exists (
    select 1
    from auth.users
    where id = auth.uid()
      and email_confirmed_at is not null
  ) then
    raise exception 'Please confirm your email before playing ranked games';
  end if;
end;
$$;

drop function if exists public.start_hourly_round(text);

create or replace function public.start_hourly_round()
returns table (
  round_id uuid,
  hour_start timestamptz,
  started_at timestamptz,
  next_playable_at timestamptz,
  completed_at timestamptz,
  won boolean,
  guess_count integer,
  server_now timestamptz,
  is_existing boolean,
  revealed_word text,
  guesses jsonb
)
language plpgsql
security definer
set search_path = public
as $$
declare
  existing_round public.wordle_rounds%rowtype;
  new_round public.wordle_rounds%rowtype;
  current_hour timestamptz := date_trunc('hour', now());
  hourly_word text := public.get_hourly_word(date_trunc('hour', now()));
begin
  perform public.require_confirmed_email();

  select *
    into existing_round
  from public.wordle_rounds
  where user_id = auth.uid()
    and public.wordle_rounds.hour_start = current_hour
  order by started_at desc
  limit 1;

  if found then
    return query select
      existing_round.id,
      existing_round.hour_start,
      existing_round.started_at,
      existing_round.next_playable_at,
      existing_round.completed_at,
      existing_round.won,
      existing_round.guess_count,
      now(),
      true,
      case when existing_round.completed_at is null then null else existing_round.word end,
      coalesce((
        select jsonb_agg(jsonb_build_object(
          'guess', wordle_guesses.guess,
          'rowIndex', wordle_guesses.row_index,
          'states', wordle_guesses.states
        ) order by wordle_guesses.row_index)
        from public.wordle_guesses
        where wordle_guesses.round_id = existing_round.id
      ), '[]'::jsonb);
    return;
  end if;

  insert into public.wordle_rounds (user_id, word, hour_start, next_playable_at)
  values (auth.uid(), hourly_word, current_hour, current_hour + interval '1 hour')
  returning * into new_round;

  return query select
    new_round.id,
    new_round.hour_start,
    new_round.started_at,
    new_round.next_playable_at,
    new_round.completed_at,
    new_round.won,
    new_round.guess_count,
    now(),
    false,
    null::text,
    '[]'::jsonb;
end;
$$;

drop function if exists public.submit_guess(uuid, text);

create or replace function public.submit_guess(target_round_id uuid, submitted_guess text)
returns table (
  guess text,
  row_index integer,
  states jsonb,
  completed boolean,
  won boolean,
  guess_count integer,
  revealed_word text,
  next_playable_at timestamptz,
  server_now timestamptz
)
language plpgsql
security definer
set search_path = public
as $$
declare
  active_round public.wordle_rounds%rowtype;
  normalized_guess text := lower(trim(submitted_guess));
  next_row integer;
  scored_states jsonb;
  did_win boolean;
  did_complete boolean;
begin
  perform public.require_confirmed_email();

  if normalized_guess !~ '^[a-z]{5}$' then
    raise exception 'Guess must be five letters';
  end if;

  if not exists (select 1 from public.wordle_dictionary where word = normalized_guess) then
    raise exception 'Not in word list';
  end if;

  select *
    into active_round
  from public.wordle_rounds rounds
  where rounds.id = target_round_id
    and rounds.user_id = auth.uid()
  limit 1;

  if not found then
    raise exception 'Round not found';
  end if;

  if active_round.completed_at is not null then
    raise exception 'Round already complete';
  end if;

  select (count(*) + 1)::integer
    into next_row
  from public.wordle_guesses
  where wordle_guesses.round_id = active_round.id;

  if next_row > 6 then
    raise exception 'No guesses remaining';
  end if;

  scored_states := public.score_guess(normalized_guess, active_round.word);
  did_win := normalized_guess = active_round.word;
  did_complete := did_win or next_row = 6;

  insert into public.wordle_guesses (round_id, user_id, guess, row_index, states)
  values (active_round.id, auth.uid(), normalized_guess, next_row, scored_states);

  if did_complete then
    update public.wordle_rounds
    set completed_at = now(),
        won = did_win,
        guess_count = next_row
    where id = active_round.id;
  end if;

  return query select
    normalized_guess,
    next_row,
    scored_states,
    did_complete,
    did_win,
    case when did_complete then next_row else null end,
    case when did_complete then active_round.word else null end,
    active_round.next_playable_at,
    now();
end;
$$;

create or replace function public.get_user_stats()
returns table (
  played integer,
  wins integer,
  current_streak integer,
  max_streak integer,
  distribution integer[]
)
language plpgsql
security definer
set search_path = public
as $$
declare
  round_record record;
  running_streak integer := 0;
  previous_hour timestamptz;
begin
  if auth.uid() is null then
    raise exception 'Authentication required';
  end if;

  played := 0;
  wins := 0;
  current_streak := 0;
  max_streak := 0;
  distribution := array[0, 0, 0, 0, 0, 0];

  for round_record in
    select won, guess_count, hour_start
    from public.wordle_rounds
    where user_id = auth.uid()
      and completed_at is not null
    order by hour_start asc
  loop
    played := played + 1;

    if round_record.won then
      wins := wins + 1;
      running_streak := case
        when running_streak > 0 and previous_hour is not null and round_record.hour_start = previous_hour + interval '1 hour'
          then running_streak + 1
        else 1
      end;
      max_streak := greatest(max_streak, running_streak);

      if round_record.guess_count between 1 and 6 then
        distribution[round_record.guess_count] := distribution[round_record.guess_count] + 1;
      end if;
    else
      running_streak := 0;
    end if;

    previous_hour := round_record.hour_start;
  end loop;

  previous_hour := null;

  for round_record in
    select won, hour_start
    from public.wordle_rounds
    where user_id = auth.uid()
      and completed_at is not null
    order by hour_start desc
  loop
    if not round_record.won then
      exit;
    end if;

    if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
      exit;
    end if;

    current_streak := current_streak + 1;
    previous_hour := round_record.hour_start;
  end loop;

  return next;
end;
$$;

create or replace function public.is_username_available(candidate_username text)
returns boolean
language plpgsql
security definer
set search_path = public
as $$
declare
  normalized_username text := nullif(lower(regexp_replace(trim(candidate_username), '[^a-z0-9_]', '', 'g')), '');
begin
  if normalized_username is null then
    return false;
  end if;

  return not exists (
    select 1
    from public.profiles
    where lower(username) = normalized_username
  );
end;
$$;

create or replace function public.get_player_current_streak(target_user_id uuid)
returns integer
language plpgsql
security definer
set search_path = public
as $$
declare
  round_record record;
  streak integer := 0;
  previous_hour timestamptz;
begin
  for round_record in
    select won, hour_start
    from public.wordle_rounds
    where user_id = target_user_id
      and completed_at is not null
    order by hour_start desc
  loop
    if not round_record.won then
      exit;
    end if;

    if previous_hour is not null and round_record.hour_start <> previous_hour - interval '1 hour' then
      exit;
    end if;

    streak := streak + 1;
    previous_hour := round_record.hour_start;
  end loop;

  return streak;
end;
$$;

create or replace function public.get_hourly_leaderboard()
returns table (
  username text,
  won boolean,
  guess_count integer,
  completed_at timestamptz,
  rank integer
)
language plpgsql
security definer
set search_path = public
as $$
begin
  return query
    select
      coalesce(profiles.username, profiles.display_name, 'player') as username,
      rounds.won,
      rounds.guess_count,
      rounds.completed_at,
      row_number() over (
        order by
          rounds.won desc,
          rounds.guess_count asc nulls last,
          rounds.completed_at asc
      )::integer as rank
    from public.wordle_rounds rounds
    left join public.profiles profiles on profiles.id = rounds.user_id
    where rounds.hour_start = date_trunc('hour', now())
      and rounds.completed_at is not null
    order by rank
    limit 25;
end;
$$;

create or replace function public.get_leaderboard(board_scope text default 'hour')
returns table (
  username text,
  won boolean,
  guess_count integer,
  completed_at timestamptz,
  rank integer,
  current_streak integer,
  is_current_user boolean,
  wins integer,
  played integer,
  average_guesses numeric
)
language plpgsql
security definer
set search_path = public
as $$
declare
  normalized_scope text := lower(coalesce(board_scope, 'hour'));
begin
  if normalized_scope = 'today' then
    return query
      with aggregate_rows as (
        select
          rounds.user_id,
          coalesce(profiles.username, profiles.display_name, 'player') as username,
          count(*)::integer as played,
          count(*) filter (where rounds.won)::integer as wins,
          round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
          min(rounds.completed_at) filter (where rounds.won) as first_win_at,
          max(rounds.completed_at) as latest_completed_at
        from public.wordle_rounds rounds
        left join public.profiles profiles on profiles.id = rounds.user_id
        where rounds.completed_at is not null
          and rounds.completed_at >= date_trunc('day', now())
        group by rounds.user_id, profiles.username, profiles.display_name
      ), ranked as (
        select
          aggregate_rows.*,
          row_number() over (
            order by aggregate_rows.wins desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
          )::integer as row_rank
        from aggregate_rows
      )
      select
        ranked.username,
        ranked.wins > 0,
        null::integer,
        ranked.latest_completed_at,
        ranked.row_rank,
        public.get_player_current_streak(ranked.user_id),
        auth.uid() = ranked.user_id,
        ranked.wins,
        ranked.played,
        ranked.average_guesses
      from ranked
      where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
      order by ranked.row_rank;
    return;
  end if;

  if normalized_scope = 'all' then
    return query
      with aggregate_rows as (
        select
          rounds.user_id,
          coalesce(profiles.username, profiles.display_name, 'player') as username,
          count(*)::integer as played,
          count(*) filter (where rounds.won)::integer as wins,
          round(avg(rounds.guess_count) filter (where rounds.won), 2) as average_guesses,
          min(rounds.completed_at) filter (where rounds.won) as first_win_at,
          max(rounds.completed_at) as latest_completed_at
        from public.wordle_rounds rounds
        left join public.profiles profiles on profiles.id = rounds.user_id
        where rounds.completed_at is not null
        group by rounds.user_id, profiles.username, profiles.display_name
      ), ranked as (
        select
          aggregate_rows.*,
          row_number() over (
            order by aggregate_rows.wins desc, public.get_player_current_streak(aggregate_rows.user_id) desc, aggregate_rows.average_guesses asc nulls last, aggregate_rows.first_win_at asc nulls last
          )::integer as row_rank
        from aggregate_rows
      )
      select
        ranked.username,
        ranked.wins > 0,
        null::integer,
        ranked.latest_completed_at,
        ranked.row_rank,
        public.get_player_current_streak(ranked.user_id),
        auth.uid() = ranked.user_id,
        ranked.wins,
        ranked.played,
        ranked.average_guesses
      from ranked
      where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
      order by ranked.row_rank;
    return;
  end if;

  return query
    with ranked as (
      select
        rounds.user_id,
        coalesce(profiles.username, profiles.display_name, 'player') as username,
        rounds.won,
        rounds.guess_count,
        rounds.completed_at,
        row_number() over (
          order by rounds.won desc, rounds.guess_count asc nulls last, rounds.completed_at asc
        )::integer as row_rank
      from public.wordle_rounds rounds
      left join public.profiles profiles on profiles.id = rounds.user_id
      where rounds.hour_start = date_trunc('hour', now())
        and rounds.completed_at is not null
    )
    select
      ranked.username,
      ranked.won,
      ranked.guess_count,
      ranked.completed_at,
      ranked.row_rank,
      public.get_player_current_streak(ranked.user_id),
      auth.uid() = ranked.user_id,
      case when ranked.won then 1 else 0 end,
      1,
      ranked.guess_count::numeric
    from ranked
    where ranked.row_rank <= 25 or auth.uid() = ranked.user_id
    order by ranked.row_rank;
end;
$$;

drop function if exists public.get_player_history(integer);

create or replace function public.get_hourly_summary()
returns table (
  completed_count integer,
  win_rate integer,
  average_guesses numeric
)
language plpgsql
security definer
set search_path = public
as $$
begin
  return query
    select
      count(*)::integer,
      coalesce(round((count(*) filter (where won)::numeric / nullif(count(*), 0)) * 100), 0)::integer,
      round(avg(guess_count) filter (where won), 2)
    from public.wordle_rounds
    where hour_start = date_trunc('hour', now())
      and completed_at is not null;
end;
$$;

drop function if exists public.get_email_for_username(text);

create or replace function public.complete_hourly_round(
  round_id uuid,
  did_win boolean,
  guess_total integer
)
returns void
language plpgsql
security definer
set search_path = public
as $$
begin
  raise exception 'complete_hourly_round is deprecated; use submit_guess';
end;
$$;

revoke all on public.wordle_words from anon, authenticated;
revoke all on public.wordle_dictionary from anon, authenticated;
revoke all on public.profiles from anon, authenticated;
revoke all on public.wordle_rounds from anon, authenticated;
revoke all on public.wordle_guesses from anon, authenticated;

grant select, update on public.profiles to authenticated;
grant select on public.wordle_rounds to authenticated;
grant select on public.wordle_guesses to authenticated;
grant execute on function public.is_username_available(text) to anon, authenticated;
grant execute on function public.require_confirmed_email() to authenticated;
grant execute on function public.start_hourly_round() to authenticated;
grant execute on function public.submit_guess(uuid, text) to authenticated;
grant execute on function public.get_user_stats() to authenticated;
grant execute on function public.get_hourly_leaderboard() to anon, authenticated;
grant execute on function public.get_leaderboard(text) to anon, authenticated;
grant execute on function public.get_hourly_summary() to anon, authenticated;
grant execute on function public.complete_hourly_round(uuid, boolean, integer) to authenticated;
