import gleam/bit_array
import gleam/crypto
import gleam/dict
import gleam/erlang/process
import gleam/int
import gleam/io
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import gleam/result
import manager
import player_session

pub type PlayerManager =
  manager.Manager(Response, Message)

pub type Message {
  AddPlayer(username: String, game_code: String)
  AttachSocketToPlayer(user_id: Int, socket_id: Int, token_hash: String)
  RemoveSocket(user_id: Int)
  RemovePlayer(user_id: Int)
  GetPlayerSocket(user_id: Int)
}

pub type PlayerError {
  PlayerNotFound(Int)
  UsernameAlreadyExists(String)
  TokenHashMistmatch
}

pub fn error_to_string(error: PlayerError) -> String {
  case error {
    PlayerNotFound(id) -> "Player of id=" <> int.to_string(id) <> " not found"
    UsernameAlreadyExists(username) ->
      "Username '" <> username <> "' already exists!"
    TokenHashMistmatch -> "Error authenticating user"
  }
}

pub type Return {
  Ack
  AddPlayerResp(user_id: Int, token: String)
  GetPlayerSocketResp(socket_id: Option(Int))
}

pub type Response =
  Result(Return, PlayerError)

pub type Request =
  manager.Request(Response, Message)

pub type InternalState {
  InternalState(
    next_id: Int,
    players: dict.Dict(Int, player_session.PlayerSession),
  )
}

pub type State =
  manager.State(InternalState)

fn call(
  player_manager: PlayerManager,
  msg: Message,
) -> Result(Return, PlayerError) {
  manager.call(player_manager, msg)
}

pub fn new() -> PlayerManager {
  manager.new(fn() { InternalState(0, dict.new()) }, handle)
}

pub fn add_player(
  player_manager: PlayerManager,
  username: String,
  gamecode: String,
) -> Result(#(Int, String), PlayerError) {
  use ret <- result.try(call(player_manager, AddPlayer(username, gamecode)))

  let assert AddPlayerResp(user_id, token) = ret
  Ok(#(user_id, token))
}

pub fn attack_socket_to_player(
  player_manager: PlayerManager,
  user_id: Int,
  socket_id: Int,
  token_hash: String,
) -> Result(Nil, PlayerError) {
  use _ret <- result.try(call(
    player_manager,
    AttachSocketToPlayer(user_id, socket_id, token_hash),
  ))

  Ok(Nil)
}

pub fn remove_socket(
  player_manager: PlayerManager,
  user_id: Int,
) -> Result(Nil, PlayerError) {
  use _ret <- result.try(call(player_manager, RemoveSocket(user_id)))

  Ok(Nil)
}

pub fn remove_player(
  player_manager: PlayerManager,
  user_id: Int,
) -> Result(Nil, PlayerError) {
  use _ret <- result.try(call(player_manager, RemovePlayer(user_id)))

  Ok(Nil)
}

pub fn get_player_socket(
  player_manager: PlayerManager,
  user_id: Int,
) -> Result(Option(Int), PlayerError) {
  use ret <- result.try(call(player_manager, GetPlayerSocket(user_id)))

  let assert GetPlayerSocketResp(socket_id) = ret

  Ok(socket_id)
}

fn handle(request: Request, state: State) -> actor.Next(Request, State) {
  let ret = case request.msg {
    AddPlayer(username, gamecode) ->
      add_player_internal(state, username, gamecode)
    AttachSocketToPlayer(user_id, socket_id, token_hash) ->
      attach_socket_to_player_internal(state, user_id, socket_id, token_hash)
    RemovePlayer(user_id) -> remove_player_internal(state, user_id)
    RemoveSocket(user_id) -> remove_socket_internal(state, user_id)
    GetPlayerSocket(user_id) -> get_player_socket_internal(state, user_id)
  }

  case ret {
    Error(err) -> {
      io.println_error("Player Manager error: " <> error_to_string(err))
      process.send(request.reply_to, Error(err))

      actor.continue(state)
    }
    Ok(#(resp, actor_state)) -> {
      process.send(request.reply_to, Ok(resp))

      actor_state
    }
  }
}

fn get_player(
  state: State,
  player_id: Int,
) -> Result(player_session.PlayerSession, PlayerError) {
  result.map_error(dict.get(state.internal.players, player_id), fn(_) {
    PlayerNotFound(player_id)
  })
}

fn add_player_to_state(
  state: State,
  player: player_session.PlayerSession,
) -> State {
  let players = dict.insert(state.internal.players, player.id, player)

  manager.State(internal: InternalState(..state.internal, players:))
}

fn add_player_internal(
  state: State,
  username: String,
  game_code: String,
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
  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 user_id = state.internal.next_id

  let sessions =
    player_session.PlayerSession(user_id, token_hash, username, None)
    |> dict.insert(state.internal.players, user_id, _)

  let continue =
    manager.State(internal: InternalState(state.internal.next_id + 1, sessions))
    |> actor.continue

  io.print("Adding" <> username <> "id=" <> int.to_string(user_id) <> "to game")
  #(AddPlayerResp(user_id, token), continue)
  |> Ok
}

fn attach_socket_to_player_internal(
  state: State,
  user_id: Int,
  socket_id: Int,
  token_hash: String,
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
  use player <- result.try(get_player(state, user_id))
  use _ <- result.try(
    case player.token_hash == player_session.hash_token(token_hash) {
      False -> Error(TokenHashMistmatch)
      True -> Ok(Nil)
    },
  )

  let state =
    player_session.PlayerSession(..player, socket_id: Some(socket_id))
    |> add_player_to_state(state, _)
    |> actor.continue

  #(Ack, state)
  |> Ok
}

fn remove_socket_internal(
  state: State,
  user_id: Int,
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
  use player <- result.try(get_player(state, user_id))

  let state =
    player_session.PlayerSession(..player, socket_id: None)
    |> add_player_to_state(state, _)
    |> actor.continue

  #(Ack, state)
  |> Ok
}

fn remove_player_internal(
  state: State,
  user_id: Int,
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
  use player <- result.try(get_player(state, user_id))

  let players = dict.delete(state.internal.players, player.id)

  let state =
    manager.State(internal: InternalState(..state.internal, players:))
    |> actor.continue

  #(Ack, state)
  |> Ok
}

fn get_player_socket_internal(
  state: State,
  user_id: Int,
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
  use player <- result.try(get_player(state, user_id))

  #(GetPlayerSocketResp(player.socket_id), actor.continue(state))
  |> Ok
}
