import gleam/dict.{type Dict}
import gleam/erlang/process.{type Subject}
import gleam/list
import gleam/otp/actor

pub type SocketManager(a) {
  SocketManager(subject: Subject(SocketManagerMsg(a)))
}

type State(a) {
  State(subjects: Dict(Int, Subject(a)), next_id: Int)
}

pub type SocketManagerMsg(a) {
  AddSocketSubject(subject: Subject(a), client: Subject(Int))
  RemoveSocketSubject(subject: Subject(a))
  GetSubjectById(id: Int, client: Subject(Result(Subject(a), Nil)))
  Shutdown
}

fn handle(
  msg: SocketManagerMsg(a),
  state: State(a),
) -> actor.Next(SocketManagerMsg(a), State(a)) {
  case msg {
    AddSocketSubject(subject, client) -> {
      let subject_id = state.next_id
      actor.send(client, subject_id)
      actor.continue(State(
        dict.insert(state.subjects, state.next_id, subject),
        state.next_id + 1,
      ))
    }
    RemoveSocketSubject(subject) -> {
      let remove_id =
        list.first(
          dict.keys(
            dict.filter(state.subjects, fn(_key, value) { value == subject }),
          ),
        )

      case remove_id {
        Ok(id) -> {
          actor.continue(
            State(..state, subjects: dict.delete(state.subjects, id)),
          )
        }
        Error(_) -> {
          actor.continue(state)
        }
      }
    }
    GetSubjectById(id, client) -> {
      actor.send(client, dict.get(state.subjects, id))
      actor.continue(state)
    }
    Shutdown -> {
      actor.Stop(process.Normal)
    }
  }
}

pub fn new() -> SocketManager(a) {
  let assert Ok(subject) = actor.start(State(dict.new(), 0), handle)

  SocketManager(subject)
}

pub fn add_socket_subj(
  socket_manager: SocketManager(a),
  subject: Subject(a),
) -> Int {
  actor.call(socket_manager.subject, AddSocketSubject(subject, _), 1000)
}

pub fn get_socket_subj_by_id(
  socket_manager: SocketManager(a),
  id: Int,
) -> Result(Subject(a), Nil) {
  actor.call(socket_manager.subject, GetSubjectById(id, _), 1000)
}

pub fn remove_socket_subj(socket_manager: SocketManager(a), subject: Subject(a)) {
  actor.send(socket_manager.subject, RemoveSocketSubject(subject))
}

pub fn shutdown(socket_manager: SocketManager(a)) {
  actor.send(socket_manager.subject, Shutdown)
}
