select create_newtype_text('public.usr_session_key');

create function usr_session_key_gen()
  returns public.usr_session_key
  volatile
  language sql as $$
select usr_session_key_of_string(
  md5(extract(epoch from now()) || gen_random_bytes(32) :: text)
);
$$;

create type usr_session_device as enum
  ( 'linux'
  , 'macos'
  , 'win'
  , 'android'
  , 'ios'
  , 'other'
  );

create table public.usr_session
  ( id          int                    not null primary key generated always as identity
  , key         public.usr_session_key not null unique
  , expired     boolean                not null default false
  , expires_at  timestamp              not null
  , usr         int                    not null references public.usr (id)
  , location    text                   null
  , device      usr_session_device     null
  , ip          inet                   null
  );

select immutable( 'public'
                , 'usr_session'
                , array[ 'id'
                       , 'key'
                       , 'expires_at'
                       , 'usr'
                       , 'location'
                       , 'device'
                       , 'ip'
                       ]
                );

create function public.usr_session_login_validate
  ( tag_or_email public.usr_tag_or_email
  , password     text
  )
  returns public.usr
  language plpgsql
  stable
  as $$
declare
  usr_email public.email   := public.usr_tag_or_email_to_email(tag_or_email);
  usr_tag   public.usr_tag := public.usr_tag_or_email_to_tag(tag_or_email);
  usr       public.usr;
begin
  select *
    from public.usr as u
   where u.email = usr_email
      or u.tag = usr_tag
    into usr;

  if usr.id = 1 or usr.tag = usr_tag_of_string('root') then
    raise notice 'root user may not be logged into';
    raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email);
  end if;

  if usr is null then
    -- prevent email guess bruteforces by raising the same exception
    -- for invalid password and user not found
    raise notice 'user % not found', usr_tag_or_email_to_string(tag_or_email);
    raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email);
  end if;

  if not hashed_text_matches(password, usr.password) then 
    raise notice 'password does not match for user %', usr_tag_or_email_to_string(tag_or_email);
    raise exception 'incorrect password for user %', usr_tag_or_email_to_string(tag_or_email);
  end if;

  return usr;
end;
$$;

create function public.usr_session_login
  ( tag_or_email public.usr_tag_or_email
  , password text
  , remember boolean                   default false
  , location text                      default null
  , device   public.usr_session_device default null
  , ip       inet                      default null
  )
  returns public.usr_session_key
  language plpgsql
  volatile
  as $$
declare
  usr        public.usr;
  key        public.usr_session_key := usr_session_key_gen();
  expires_at timestamp;
begin
  usr := public.usr_session_login_validate(tag_or_email, password);

  if remember then
    expires_at := now() + interval '1 week';
  else
    expires_at := now() + interval '1 hour';
  end if;

  insert into public.usr_session
    (key, expires_at, usr, location, device, ip)
  values
    (key, expires_at, usr.id, location, device, ip);

  return key;
end;
$$;
