import app/clients/host_client
import app/clients/player_client
import gleam/dict
import gleam/erlang/process
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/otp/actor
import gleam/result
import manager
import player
import player_manager
import session
import socket_manager

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

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

pub type State =
  manager.State(InternalState)

pub type InternalState {
  InternalState(
    games: dict.Dict(String, session.Session),
    player_manager: player_manager.PlayerManager,
    socket_manager: socket_manager.SocketManager(String),
  )
}

pub fn new(player_manager, socket_manager) -> SessionManager {
  manager.new(
    fn() { InternalState(dict.new(), player_manager, socket_manager) },
    handle,
  )
}

pub type Message {
  CreateNewGame(game_id: String, host_id: Int)
  EndGame(game_id: String)
  AddPlayerToGame(game_id: String, player: player.Player)
  RemovePlayerFromGame(game_id: String, player_id: Int)
  BuzzInPlayer(game_id: String, player_id: Int, time: Float)
  ResetAllBuzers(game_id: String)
  ResetPlayerBuzzer(game_id: String, player_id: Int)
  UpdatePlayerScore(game_id: String, player_id: Int, score_diff: Int)

  Shutdown
}

pub type Response =
  Result(Nil, SessionError)

pub type SessionError {
  GameAlreadyExists(game_id: String)
  GameNotFound(game_id: String)
  PlayerNotFound(player_id: Int)
}

pub fn error_to_string(error: SessionError) -> String {
  case error {
    GameAlreadyExists(game_id) ->
      "Game with id=" <> game_id <> " already exists."
    GameNotFound(game_id) -> "Game with id=" <> game_id <> " not found."
    PlayerNotFound(player_id) ->
      "Player with id=" <> int.to_string(player_id) <> " not found."
  }
}

fn call(
  session_manager: SessionManager,
  msg: Message,
) -> Result(Nil, SessionError) {
  manager.call(session_manager, msg)
}

pub fn create_new_game(
  session_manager: SessionManager,
  game_id: String,
  host_id: Int,
) -> Result(Nil, SessionError) {
  call(session_manager, CreateNewGame(game_id, host_id))
}

pub fn end_game(
  session_manager: SessionManager,
  game_id: String,
) -> Result(Nil, SessionError) {
  call(session_manager, EndGame(game_id))
}

pub fn add_player_to_game(
  session_manager: SessionManager,
  game_id: String,
  player: player.Player,
) -> Result(Nil, SessionError) {
  call(session_manager, AddPlayerToGame(game_id, player))
}

pub fn remove_player_from_game(
  session_manager: SessionManager,
  game_id: String,
  player_id: Int,
) -> Result(Nil, SessionError) {
  call(session_manager, RemovePlayerFromGame(game_id, player_id))
}

pub fn buzz_in_player(
  session_manager: SessionManager,
  game_id: String,
  player_id: Int,
  time: Float,
) -> Result(Nil, SessionError) {
  call(session_manager, BuzzInPlayer(game_id, player_id, time))
}

pub fn reset_all_buzzers(
  session_manager: SessionManager,
  game_id: String,
) -> Result(Nil, SessionError) {
  call(session_manager, ResetAllBuzers(game_id))
}

pub fn reset_player_buzzer(
  session_manager: SessionManager,
  game_id: String,
  user_id: Int,
) -> Result(Nil, SessionError) {
  call(session_manager, ResetPlayerBuzzer(game_id, user_id))
}

pub fn updater_player_score(
  session_manager: SessionManager,
  game_id: String,
  user_id: Int,
  score_diff: Int,
) -> Result(Nil, SessionError) {
  call(session_manager, UpdatePlayerScore(game_id, user_id, score_diff))
}

fn get_game(state: State, game_id) -> Result(session.Session, SessionError) {
  result.map_error(dict.get(state.internal.games, game_id), fn(_) {
    GameNotFound(game_id)
  })
}

fn get_player_in_game(
  game: session.Session,
  player_id: Int,
) -> Result(player.Player, SessionError) {
  result.map_error(dict.get(game.players, player_id), fn(_) {
    PlayerNotFound(player_id)
  })
}

fn add_game_to_manager(
  state: State,
  game_id: String,
  game: session.Session,
) -> State {
  send_message_to_host(
    state,
    game.host_user_id,
    host_client.UpdatePlayerStates(game.players),
  )

  let games = dict.insert(state.internal.games, game_id, game)

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

fn add_player_to_game_internal(
  game: session.Session,
  player: player.Player,
) -> session.Session {
  session.Session(..game, players: dict.insert(game.players, player.id, player))
}

// Actor Logic

fn get_player_session_subject(
  state: State,
  player_id: Int,
) -> Option(process.Subject(String)) {
  let assert Ok(socket_id) =
    player_manager.get_player_socket(state.internal.player_manager, player_id)
  case socket_id {
    None -> None
    Some(id) -> {
      let assert Ok(subject) =
        socket_manager.get_socket_subj_by_id(state.internal.socket_manager, id)

      Some(subject)
    }
  }
}

fn send_message_to_socket(state: State, player_id: Int, msg: String) {
  let subject = get_player_session_subject(state, player_id)

  case subject {
    None -> Nil
    Some(subject) -> {
      actor.send(subject, msg)
    }
  }
}

fn send_message_to_player(
  state: State,
  player_id: Int,
  msg: player_client.PlayerServerMessages,
) {
  player_client.encode_player_server_messages(msg)
  |> json.to_string
  |> send_message_to_socket(state, player_id, _)
}

fn broadcast_msg_to_players(
  state: State,
  game: session.Session,
  msg: player_client.PlayerServerMessages,
) {
  game.players
  |> dict.keys
  |> list.each(fn(player_id) { send_message_to_player(state, player_id, msg) })
}

fn send_message_to_host(
  state: State,
  player_id: Int,
  msg: host_client.HostServerMessages,
) {
  host_client.encode_host_server_messages(msg)
  |> json.to_string
  |> send_message_to_socket(state, player_id, _)
}

fn add_player_internal(
  state: State,
  game_id: String,
  player: player.Player,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))

  add_player_to_game_internal(game, player)
  |> add_game_to_manager(state, game_id, _)
  |> actor.continue
  |> Ok
}

fn buzz_in_player_internal(
  state: State,
  game_id: String,
  player_id: Int,
  time: Float,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))
  use player <- result.try(get_player_in_game(game, player_id))

  player.Player(..player, buzz_in_time: Some(time))
  |> add_player_to_game_internal(game, _)
  |> add_game_to_manager(state, game_id, _)
  |> actor.continue
  |> Ok
}

fn create_new_game_internal(
  state: State,
  game_id: String,
  host_id: Int,
) -> Result(actor.Next(Request, State), SessionError) {
  case dict.has_key(state.internal.games, game_id) {
    True -> Error(GameAlreadyExists(game_id))
    False -> {
      session.Session(game_id, host_id, dict.new())
      |> add_game_to_manager(state, game_id, _)
      |> actor.continue
      |> Ok
    }
  }
}

fn end_game_internal(
  state: State,
  game_id: String,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))

  broadcast_msg_to_players(state, game, player_client.ExitGame)
  send_message_to_host(state, game.host_user_id, host_client.ExitGame)

  game.players
  |> dict.each(fn(key, _) {
    let _ = player_manager.remove_player(state.internal.player_manager, key)
  })

  let _ =
    player_manager.remove_player(
      state.internal.player_manager,
      game.host_user_id,
    )

  let games = dict.delete(state.internal.games, game_id)

  manager.State(internal: InternalState(..state.internal, games:))
  |> actor.continue
  |> Ok
}

fn remove_player_from_game_internal(
  state: State,
  game_id: String,
  player_id: Int,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))
  use _ <- result.try(get_player_in_game(game, player_id))

  let players = dict.delete(game.players, player_id)

  session.Session(..game, players: players)
  |> add_game_to_manager(state, game_id, _)
  |> actor.continue
  |> Ok
}

fn reset_all_buzzers_internal(
  state: State,
  game_id: String,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))

  let players =
    dict.map_values(game.players, fn(_, player) {
      player.Player(..player, buzz_in_time: None)
    })

  broadcast_msg_to_players(state, game, player_client.ResetBuzzer)

  session.Session(..game, players: players)
  |> add_game_to_manager(state, game_id, _)
  |> actor.continue
  |> Ok
}

fn reset_player_buzzer_internal(
  state: State,
  game_id: String,
  player_id: Int,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))
  use player <- result.try(get_player_in_game(game, player_id))

  send_message_to_player(state, player.id, player_client.ResetBuzzer)

  player.Player(..player, buzz_in_time: None)
  |> add_player_internal(state, game_id, _)
}

fn update_player_score_internal(
  state: State,
  game_id: String,
  player_id: Int,
  score_diff: Int,
) -> Result(actor.Next(Request, State), SessionError) {
  use game <- result.try(get_game(state, game_id))
  use player <- result.try(get_player_in_game(game, player_id))

  let player = player.Player(..player, score: player.score + score_diff)

  send_message_to_player(
    state,
    player_id,
    player_client.UpdatePointTotal(player.score),
  )

  player
  |> add_player_internal(state, game_id, _)
}

fn handle(request: Request, state: State) -> actor.Next(Request, State) {
  let ret = case request.msg {
    AddPlayerToGame(game_id, player) -> {
      add_player_internal(state, game_id, player)
    }
    BuzzInPlayer(game_id, player_id, time) -> {
      buzz_in_player_internal(state, game_id, player_id, time)
    }
    CreateNewGame(game_id, host_id) -> {
      create_new_game_internal(state, game_id, host_id)
    }
    EndGame(game_id) -> {
      end_game_internal(state, game_id)
    }
    RemovePlayerFromGame(game_id, player_id) -> {
      remove_player_from_game_internal(state, game_id, player_id)
    }
    ResetAllBuzers(game_id) -> {
      reset_all_buzzers_internal(state, game_id)
    }
    ResetPlayerBuzzer(game_id, player_id) -> {
      reset_player_buzzer_internal(state, game_id, player_id)
    }
    UpdatePlayerScore(game_id, player_id, score_diff) -> {
      update_player_score_internal(state, game_id, player_id, score_diff)
    }
    Shutdown -> {
      Ok(actor.Stop(process.Normal))
    }
  }

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

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

      actor_state
    }
  }
}
