import app/web
import gleam/dict
import gleam/erlang/process
import gleam/int
import gleam/io
import gleam/option.{None, Some}
import gleam/otp/actor
import gleam/result
import player
import session

pub type SessionManager {
  SessionManager(subject: process.Subject(Request))
}

pub fn new(ctx: web.Context) -> SessionManager {
  let assert Ok(subject) = actor.start(State(ctx, dict.new()), handle)

  SessionManager(subject)
}

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 Request {
  Request(reply_with: process.Subject(Response), msg: Message)
}

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_sring(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."
  }
}

type State {
  State(ctx: web.Context, games: dict.Dict(String, session.Session))
}

fn call(
  session_manager: SessionManager,
  msg: Message,
) -> Result(Nil, SessionError) {
  actor.call(session_manager.subject, Request(_, msg), 1000)
}

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_buzzers(
  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.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 {
  let games = dict.insert(state.games, game_id, game)

  State(..state, games: 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 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.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 _ <- result.try(get_game(state, game_id))

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

  State(..state, games: 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)
    })

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

fn reset_player_buzzer(
  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))

  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))

  player.Player(..player, score: player.score + score_diff)
  |> 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(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_sring(err))
      process.send(request.reply_with, Error(err))

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

      actor_state
    }
  }
}
