import app/clients/host_client
import app/clients/new_user_client
import app/clients/player_client
import config
import gleam/bit_array
import gleam/crypto
import gleam/dict
import gleam/erlang/process
import gleam/float
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import player
import player_session
import session
import socket_manager
import storail

pub type Error {
  InvalidToken
}

pub type Context {
  Context(
    config: config.Config,
    sessions: storail.Collection(session.Session),
    player_sessions: storail.Collection(player_session.PlayerSession),
    socket_manager: socket_manager.SocketManager(String),
  )
}

pub type ClientState {
  Host(host_client.HostState)
  Player(player_client.PlayerState)
  NewUser(new_user_client.NewUserState)
}

pub type SocketState {
  SocketState(
    ctx: Context,
    id: Int,
    subject: process.Subject(String),
    state: ClientState,
  )
}

pub fn handle_client_msg(
  socket_state: SocketState,
  msg: String,
) -> #(SocketState, String) {
  case socket_state.state {
    Host(state) -> handle_host(socket_state, state, msg)
    NewUser(state) -> handle_new_user(socket_state, state, msg)
    Player(state) -> handle_player(socket_state, state, msg)
  }
}

fn set_buzz_in(
  socket_state: SocketState,
  player_state: player_client.PlayerState,
  time: Float,
) {
  let key = storail.key(socket_state.ctx.sessions, player_state.game_id)
  let assert Ok(session) = storail.read(key)

  let players =
    dict.upsert(session.players, player_state.user_id, fn(player) {
      let assert Some(player) = player

      player.Player(..player, buzz_in_time: Some(time))
    })

  let session = session.Session(..session, players:)

  let assert Ok(_) = storail.write(key, session)

  send_message_to_host(
    socket_state.ctx,
    session.host_user_id,
    host_client.UpdatePlayerStates(session.players),
  )
}

fn handle_player(
  socket_state: SocketState,
  player_state: player_client.PlayerState,
  msg: String,
) -> #(SocketState, String) {
  let assert Ok(msg) =
    json.parse(msg, player_client.player_client_messages_decoder())

  case msg {
    player_client.BuzzIn(time) -> {
      io.println(
        "Got buzz in from "
        <> int.to_string(player_state.user_id)
        <> " at "
        <> float.to_string(time),
      )

      set_buzz_in(socket_state, player_state, time)

      #(
        socket_state,
        json.to_string(player_client.encode_player_server_messages(
          player_client.Ack,
        )),
      )
    }
  }
}

pub fn register_user(
  ctx: Context,
  username: String,
  game_code: String,
) -> #(String, Int) {
  let secret = crypto.strong_random_bytes(16)
  let token =
    crypto.new_hasher(crypto.Sha512)
    |> crypto.hash_chunk(secret)
    |> crypto.hash_chunk(bit_array.from_string(username))
    |> crypto.hash_chunk(bit_array.from_string(game_code))
    |> crypto.digest
    |> bit_array.base64_encode(True)

  let token_hash = player_session.hash_token(token)

  let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8)

  let key = storail.key(ctx.player_sessions, int.to_base16(user_id))

  let assert Ok(Nil) =
    storail.write(
      key,
      player_session.PlayerSession(
        id: user_id,
        token_hash:,
        username:,
        socket_id: None,
      ),
    )

  #(token, user_id)
}

pub fn attach_socket_to_user(
  state: SocketState,
  user_id: Int,
  token: String,
) -> Result(Nil, Error) {
  let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
  let assert Ok(player_state) = storail.read(key)

  let hashed_token = player_session.hash_token(token)

  case hashed_token == player_state.token_hash {
    False -> Error(InvalidToken)
    True -> {
      let assert Ok(Nil) =
        storail.write(
          key,
          player_session.PlayerSession(
            ..player_state,
            socket_id: Some(state.id),
          ),
        )

      Ok(Nil)
    }
  }
}

fn handle_new_user(
  socket_state: SocketState,
  _new_user_state: new_user_client.NewUserState,
  msg: String,
) -> #(SocketState, String) {
  let assert Ok(msg) =
    json.parse(msg, new_user_client.new_user_client_messages_decoder())

  let #(socket_state, new_user_msg) = case msg {
    new_user_client.JoinAsHost(token, user_id, game_id) -> {
      let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)

      #(
        SocketState(
          ..socket_state,
          state: Host(host_client.HostState(user_id:, game_id:)),
        ),
        new_user_client.HostRegisterResponse,
      )
    }
    new_user_client.JoinAsPlayer(token, user_id, game_id) -> {
      let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)

      #(
        SocketState(
          ..socket_state,
          state: Player(player_client.PlayerState(user_id:, game_id:)),
        ),
        new_user_client.RegisterResponse,
      )
    }
  }

  #(
    socket_state,
    json.to_string(new_user_client.encode_new_user_server_messages(new_user_msg)),
  )
}

fn get_player_session_from_id(
  ctx: Context,
  id: Int,
) -> player_session.PlayerSession {
  let player_key = storail.key(ctx.player_sessions, int.to_base16(id))
  let assert Ok(player) = storail.read(player_key)
  player
}

fn get_player_session_subject(
  ctx: Context,
  player: player_session.PlayerSession,
) -> Option(process.Subject(String)) {
  case player.socket_id {
    None -> None
    Some(id) -> {
      let assert Ok(subject) =
        socket_manager.get_socket_subj_by_id(ctx.socket_manager, id)

      Some(subject)
    }
  }
}

pub fn broadcast_message_to_players(
  ctx: Context,
  session: session.Session,
  msg: player_client.PlayerServerMessages,
) {
  dict.keys(session.players)
  |> list.map(get_player_session_from_id(ctx, _))
  |> list.map(get_player_session_subject(ctx, _))
  |> list.filter_map(fn(subject) { option.to_result(subject, "Missing") })
  |> list.each(fn(subject) {
    let client_msg = player_client.encode_player_server_messages(msg)
    actor.send(subject, json.to_string(client_msg))
  })
}

pub fn send_message_to_player(
  ctx: Context,
  id: Int,
  msg: player_client.PlayerServerMessages,
) {
  let client_msg = player_client.encode_player_server_messages(msg)

  let subject =
    get_player_session_from_id(ctx, id)
    |> get_player_session_subject(ctx, _)

  case subject {
    None -> Nil
    Some(subject) -> {
      actor.send(subject, json.to_string(client_msg))
    }
  }
}

pub fn send_message_to_host(
  ctx: Context,
  id: Int,
  msg: host_client.HostServerMessages,
) {
  let client_msg = host_client.encode_host_server_messages(msg)

  let subject =
    get_player_session_from_id(ctx, id)
    |> get_player_session_subject(ctx, _)

  case subject {
    None -> Nil
    Some(subject) -> {
      actor.send(subject, json.to_string(client_msg))
    }
  }
}

fn reset_player_buzzer(
  socket_state: SocketState,
  session_key: storail.Key(session.Session),
  id: Int,
) {
  let assert Ok(session) = storail.read(session_key)

  let assert Ok(player) = dict.get(session.players, id)
  let player = player.Player(..player, buzz_in_time: None)

  let players = dict.insert(session.players, id, player)

  let session = session.Session(..session, players: players)

  let assert Ok(_) = storail.write(session_key, session)

  send_message_to_host(
    socket_state.ctx,
    session.host_user_id,
    host_client.UpdatePlayerStates(session.players),
  )
}

fn handle_host(
  socket_state: SocketState,
  host_state: host_client.HostState,
  msg: String,
) -> #(SocketState, String) {
  let assert Ok(msg) =
    json.parse(msg, host_client.host_client_messages_decoder())

  let session_key = storail.key(socket_state.ctx.sessions, host_state.game_id)
  let assert Ok(session) = storail.read(session_key)

  let #(socket_state, resp) = case msg {
    host_client.ResetAllBuzers -> {
      dict.to_list(session.players)
      |> list.each(fn(player_entry) {
        reset_player_buzzer(socket_state, session_key, player_entry.0)
      })

      broadcast_message_to_players(
        socket_state.ctx,
        session,
        player_client.ResetBuzzer,
      )
      #(socket_state, host_client.Ack)
    }
    host_client.ResetPlayerBuzzer(id) -> {
      reset_player_buzzer(socket_state, session_key, id)
      send_message_to_player(socket_state.ctx, id, player_client.ResetBuzzer)
      #(socket_state, host_client.Ack)
    }
    host_client.UpdateScore(id, score) -> {
      let assert Ok(player) = dict.get(session.players, id)
      let player = player.Player(..player, score: player.score + score)
      let session =
        session.Session(
          ..session,
          players: dict.insert(session.players, player.id, player),
        )
      let assert Ok(_) = storail.write(session_key, session)

      send_message_to_player(
        socket_state.ctx,
        id,
        player_client.UpdatePointTotal(player.score),
      )

      send_message_to_host(
        socket_state.ctx,
        session.host_user_id,
        host_client.UpdatePlayerStates(session.players),
      )

      #(socket_state, host_client.Ack)
    }
  }

  let resp = json.to_string(host_client.encode_host_server_messages(resp))

  #(socket_state, resp)
}
