create or replace function create_user(
    p_name varchar,
    -- p_discord_id numeric(20,0),
    p_wallet varchar(64),
    p_user_id uuid default null
) returns uuid
as $$
declare
  new_uuid uuid;
  res_id integer;
  currency_start_amount float8;
begin
  if p_user_id is null then
    insert into users(name, wallet) values (p_name, p_wallet)
    returning id into new_uuid;
  else
    new_uuid := p_user_id;
    insert into users(id, name, wallet) values (p_user_id, p_name, p_wallet);
  end if;

  select value into currency_start_amount
  from game_constants
  where key = 'MoonbucksStartAmount';

  insert into bank_account(user_id, balance) values (new_uuid, currency_start_amount);

  insert into resource_account (resource_id, user_id, balance)
  select resource.id, new_uuid, 50
  from resource
  left join lateral (values (id)) as subquery(value) on true;

  return new_uuid;
end;
$$ language plpgsql;

create or replace function get_accounts(user_id uuid)
returns table (
  id uuid,
  "primaryBalance" float8,
  "resourceAccounts" json
)
as $$
begin
  return query
    select
      bank_account.id,
      bank_account.balance,
      json_agg(
        json_build_object(
        'id', resource_account.id,
        'resourceType', resource.name,
        'balance', resource_account.balance)
        order by resource.name)
    from bank_account
    join resource_account on bank_account.user_id = resource_account.user_id
    join resource on resource.id = resource_account.resource_id
    where bank_account.user_id = $1
    group by bank_account.id;
end;
$$ language plpgsql;


create or replace function sell_resources(
  p_user_id uuid,
  p_resources jsonb
)
returns table (
  "resourceName" text,
  "resourceAmount" float8,
  "returnAmount" float8,
  "saleResult" text
)
as $$
declare
  resource jsonb;
  resource_name text;
  resource_amount float8;
  return_amount float8;
  resource_sell_factor real;
  sale_result text;
begin
  for resource in select * from jsonb_array_elements(p_resources)
  loop
    resource_name := resource ->> 'resourceType';
    resource_amount := (resource ->> 'resourceAmount')::float8;
    sale_result := null;

    begin
      update resource_account
      set balance = balance - resource_amount
      where user_id = p_user_id and resource_id = (
        select id from resource where name = resource_name
      );
    exception
    -- TODO: Test this out to see if it is properly returned
      when others then
        sale_result := 'Error: Insufficient amount';
    end;

    if sale_result is null then
      select value into resource_sell_factor
      from game_constants
      where key = resource_name || 'ToMoonbucks';

      update bank_account
      set balance = balance + resource_amount / resource_sell_factor
      where user_id = p_user_id
      returning resource_amount * resource_sell_factor into return_amount;

      sale_result := 'Success';
    end if;
    return query select resource_name, resource_amount, return_amount, sale_result;
  end loop;
end;
$$ language plpgsql;

create or replace function clear_user_data(p_user_id uuid)
returns void
as $$
begin
  delete from staking_source where user_id = p_user_id;
  delete from inventory_item where user_id = p_user_id;
  update bank_account set balance = 2000 where user_id = p_user_id;
  update resource_account set balance = 0 where user_id = p_user_id;
end;
$$ language plpgsql;

create or replace function get_game_config()
returns table (
  resources json,
  "gameConstants" json,
  "stakingSources" json,
  "storeItems" json
)
as $$
begin
  select json_agg(name) into resources from resource;
  select json_object_agg(key, value) into "gameConstants" from game_constants;
  select json_agg(staking_source_item) into "stakingSources" from staking_source_item;
  select json_agg(
    json_build_object(
      'id', store_item.id,
      'name', store_item.name,
      'description', store_item.description,
      'price', store_item.price,
      'image', store_item.image_name,
      'claimAmount', store_item.claim_amount,
      'completionTimeInMins', store_item.completion_time_in_mins,
      'upgrades', upgrades
    ) order by store_item.id
  ) into "storeItems"
  from store_item
  join (
    select store_item_id, json_agg(
      json_build_object(
        'tier', tier,
        'price', upgrade_item.price,
        'claimBoost', claim_boost
      ) order by upgrade_item.tier
    ) as upgrades
    from upgrade_item
    group by store_item_id
  ) u on u.store_item_id = store_item.id;
  return next;

end;
$$ language plpgsql;

create or replace function get_staking_sources(user_id uuid)
returns table (
  id uuid,
  name varchar,
  description varchar,
  "imageName" text,
  "resourceWells" json,
  "activeStakes" json
)
as $$
begin
  return query
    select
      staking_source.id as id,
      staking_source.name,
      staking_source.description,
      staking_source.image_name,
      json_agg(
        json_build_object(
          'id', resource_well.id,
          'resourceType', resource.name,
          'supply', resource_well.supply
        )
      ) as "resourceWells",
      COALESCE (json_agg(
        json_build_object(
          'id', se.id,
          'resourceType', resource.name,
          'inventoryItemId', se.inventory_item_id,
          'durationInMins', se.duration_in_mins,
          'stakeAmount', se.stake_amount,
          'startTime', se.created_at
        )
      ) filter (where se.id is not null), '[]'::json) as "activeStakes"
    from staking_source
    inner join resource_well on resource_well.source_id = staking_source.id
    inner join resource on resource.id = resource_well.resource_id
    left join (
      select staking_event.id, staking_event.well_id, staking_event.created_at,
             duration_in_mins, stake_amount, inventory_item_id
      from staking_event
      left join claim_event on claim_event.staking_event_id = staking_event.id
      where claim_event.staking_event_id is null
    ) se on se.well_id = resource_well.id
    where staking_source.user_id = $1
    group by staking_source.id;
end;
$$ language plpgsql;

create or replace function create_staking_source(p_user_id uuid)
returns uuid
as $$
declare
  source_item record;
  new_source_id uuid;
  res_id integer;
  address varchar;
  price float8;
  min_res float8;
  max_res float8;
begin
  select value into price from game_constants where key = 'price';

  begin
    update bank_account set balance = balance - price where user_id = p_user_id;
  exception
    when check_violation then
        raise exception 'Insufficient funds';
  end;

  select * into source_item from staking_source_item order by random() limit 1;
  select substr(md5(random()::text), 1, 16) AS random_string into address;
  select value into min_res from game_constants where key = 'resourceMinStartAmount';
  select value into max_res from game_constants where key = 'resourceMaxStartAmount';

  insert into staking_source(user_id, name, description, image_name, address)
  values (
    p_user_id,
    source_item.name,
    source_item.description,
    source_item.image_name,
    '0x' || address
  )
  returning id into new_source_id;

  insert into resource_well (source_id, resource_id, supply)
  select new_source_id, resource.id, random_supply
  from resource,
    (select floor(random() * (max_res - min_res) + min_res) as random_supply)
  subquery
  left join lateral (values(id)) subquery2(value) on true;

  return new_source_id as result;
end;
$$ language plpgsql;

create or replace function get_leaderboard(top_n integer)
returns table (
  id uuid,
  name varchar,
  wallet varchar,
  balance float8
)
as $$
begin
  return query
    select users.id, users.name, users.wallet, bank_account.balance
    from users
    join bank_account on bank_account.user_id = users.id
    order by balance desc
    limit top_n;
end;
$$ language plpgsql;


create or replace function purchase_item(
  p_user_id uuid,
  p_store_item_id integer
) returns uuid
as $$
declare
  new_item_id uuid;
  item_price float8;
begin
  select price into item_price from store_item where store_item.id = p_store_item_id;

  begin
    update bank_account set balance = balance - item_price where user_id = p_user_id;
  exception
    when check_violation then
        raise exception 'Insufficient funds';
  end;

  begin
    insert into inventory_item (user_id, store_item_id) values (p_user_id, p_store_item_id)
    returning id into new_item_id;
  exception
    when unique_violation then
        raise exception 'User already owns this weapons';
  end;

  return new_item_id;
end;
$$ language plpgsql;

create or replace function upgrade_item(
  p_user_id uuid,
  p_item_id uuid
) returns uuid
as $$
declare
  upgrade_event_id uuid;
  item_insert_id uuid;
  upgrade_price float8;
begin

  select upgrade_item.price into upgrade_price
  from inventory_item
  join store_item on inventory_item.store_item_id = store_item.id
  join upgrade_item on store_item.id = upgrade_item.store_item_id
  where inventory_item.id = p_item_id and upgrade_item.tier = inventory_item.tier + 1;

  if upgrade_price is null then
    raise exception 'Inventory item not found';
  end if;

  begin
    update bank_account set balance = balance - upgrade_price where user_id = p_user_id;
  exception
    when check_violation then
        raise exception 'Insufficient funds';
  end;

  update inventory_item set tier = tier + 1
  where inventory_item.id = p_item_id and inventory_item.user_id = p_user_id
  returning id into item_insert_id;

  if item_insert_id is null then
    raise exception 'Inventory item does not belong to the user';
  end if;

  insert into upgrade_event(inventory_item_id) values (p_item_id) returning id into upgrade_event_id;
  return upgrade_event_id;

end;
$$ language plpgsql;

create or replace function stake(
  p_user_id uuid,
  p_well_id uuid,
  p_inventory_item_id uuid
) returns uuid
as $$
declare
  staking_event_id uuid;
  item_stake_duration integer;
  stake_end_time timestamp;
  inv_id uuid;
  stake_id uuid;
  stake_created_at timestamp;
  total_stake_amount float8;
  found_resource_well_id uuid;
begin

  select completion_time_in_mins
  into item_stake_duration
  from inventory_item
  join store_item on inventory_item.store_item_id = store_item.id
  where inventory_item.id = p_inventory_item_id;

  select
    inventory_item.id,
    staking_event.id,
    staking_event.created_at
  from inventory_item
  left join staking_event
    on staking_event.inventory_item_id = inventory_item.id
  where inventory_item.id = p_inventory_item_id and inventory_item.user_id = p_user_id
  order by stake_created_at desc limit 1
  into inv_id, stake_id, stake_created_at;

  if inv_id is null then
    raise exception 'Unable to find an inventory item that belongs to the user';
  end if;

  stake_end_time := stake_created_at + (item_stake_duration * interval '1 minute');

  if stake_id is not null and now() at time zone 'utc' < stake_end_time then
    raise exception 'Unable to find an inventory item that is owned and not actively staked';
  end if;

  select claim_amount
  into total_stake_amount
  from inventory_item
  join store_item on inventory_item.store_item_id = store_item.id
  left join upgrade_item on store_item.id = upgrade_item.store_item_id
                       and upgrade_item.tier = inventory_item.tier
  where inventory_item.id = p_inventory_item_id;

  select resource_well.id
  into found_resource_well_id
  from resource_well
  join staking_source on source_id = staking_source.id
  where user_id = p_user_id and resource_well.id = p_well_id;

  if not found then
    raise exception 'Resource well does not belong to this user';
  end if;

  insert into staking_event(user_id, well_id, inventory_item_id, duration_in_mins, stake_amount)
  values (p_user_id, p_well_id, p_inventory_item_id, item_stake_duration, total_stake_amount)
  returning id into staking_event_id;

  return staking_event_id;
end;
$$ language plpgsql;

create or replace function get_all_stakes(p_user_id uuid)
returns table (
  id uuid,
  "sourceId" uuid,
  "wellId" uuid,
  "inventoryItemId" uuid,
  "resourceType" varchar,
  "stakeAmount" float8,
  "stakeTime" timestamp with time zone,
  "durationInMins" integer,
  unclaimed bool
)
as $$
begin
  return query
    select
      staking_event.id,
      staking_source.id as sourceId,
      resource_well.id as wellId,
      inventory_item_id as inventoryItemId,
      resource.name as resourceType,
      stake_amount as stakeAmount,
      staking_event.created_at as stakeTime,
      duration_in_mins as durationInMins,
      case when claim_event.staking_event_id is null then true else false end as unclaimed
    from staking_event
    inner join resource_well on well_id = resource_well.id
    inner join resource on resource.id = resource_well.resource_id
    inner join staking_source on source_id = staking_source.id
    left join claim_event on staking_event.id = claim_event.staking_event_id
    where staking_event.user_id = p_user_id;
end;
$$ language plpgsql;

create or replace function claim(p_user_id uuid, p_stake_id uuid)
returns uuid
as $$
declare
  stake record;
  claim_event_id uuid;
  stake_end_time timestamp;
  resource_supply float8;
  final_supply float8;
begin
  select
    staking_event.id,
    resource.id as res_id,
    staking_event.user_id,
    well_id,
    inventory_item_id,
    duration_in_mins,
    stake_amount,
    created_at
  into stake from staking_event
  join resource_well on resource_well.id = well_id
  join resource on resource.id = resource_well.resource_id
  where staking_event.id = p_stake_id and staking_event.user_id = p_user_id;

  if stake is null then
    raise exception sqlstate '90001'
    using message = 'Staking event not found';
  end if;

  if (select id from claim_event where staking_event_id = stake.id) is not null then
    raise exception sqlstate '90001'
    using message = 'Stake already claimed';
  end if;

  stake_end_time := stake.created_at + (stake.duration_in_mins * interval '1 minute');

  if now() at time zone 'utc' < stake_end_time then
    raise exception sqlstate '90001'
    using message = 'Stake has not finished, cannot claim';
  end if;

  select supply from resource_well where id = stake.well_id into resource_supply;

  final_supply := case when resource_supply > stake.stake_amount
    then stake.stake_amount
    else resource_supply
    end;

  update resource_account set balance = balance + final_supply
  where user_id = p_user_id and resource_id = stake.res_id;

  update resource_well set supply = supply - final_supply
  where resource_id = stake.res_id;

  insert into claim_event(staking_event_id, claim_amount) values (stake.id, final_supply)
  returning id into claim_event_id;

  return claim_event_id;
end;
$$ language plpgsql;
