defmodule ShootingEventPhxWeb.ApiController do use ShootingEventPhxWeb, :controller alias ShootingEventPhx.{Auth, Events} alias ShootingEventPhx.Auth.SessionStore alias ShootingEventPhx.Domain.{EventData, Settings, Stages, StateService} alias ShootingEventPhx.AI.Gemini def health(conn, _params) do json(conn, %{"status" => "ok"}) end def state(conn, _params) do state = StateService.read_state(Auth.admin_request?(conn)) json(conn, state) end def admin_state(conn, _params) do json(conn, StateService.read_state(true)) end def admin_login(conn, params) do username = Map.get(params, "username", "") password = Map.get(params, "password", "") if Auth.verify_admin(username, password) do {:ok, token, expires_at} = SessionStore.create_token() json(conn, %{ "token" => token, "expiresAt" => DateTime.to_iso8601(expires_at), "username" => Auth.admin_user() }) else error(conn, :unauthorized, "invalid credentials") end end def admin_logout(conn, _params) do case Auth.bearer_token(conn) do nil -> :ok token -> SessionStore.delete_token(token) end json(conn, %{"ok" => true}) end def update_settings(conn, params) do case Settings.update_settings(params) do :ok -> Events.broadcast() json(conn, StateService.read_state(true)) {:error, message} -> error(conn, :bad_request, message) end end def create_player(conn, params) do case EventData.create_player(params) do :ok -> Events.broadcast() conn |> put_status(:created) |> json(StateService.read_state(true)) {:error, :bad_request, message} -> error(conn, :bad_request, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def update_player(conn, %{"id" => id} = params) do with {player_id, ""} <- Integer.parse(to_string(id)), result <- EventData.update_player(player_id, params), :ok <- result do Events.broadcast() json(conn, StateService.read_state(true)) else :error -> error(conn, :bad_request, "invalid player id") {:error, :bad_request, message} -> error(conn, :bad_request, message) {:error, :not_found, message} -> error(conn, :not_found, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def delete_player(conn, %{"id" => id}) do with {player_id, ""} <- Integer.parse(to_string(id)), :ok <- EventData.delete_player(player_id) do Events.broadcast() json(conn, StateService.read_state(true)) else :error -> error(conn, :bad_request, "invalid player id") {:error, :not_found, message} -> error(conn, :not_found, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def auto_group_players(conn, params) do groups = Map.get(params, "groups", []) case EventData.auto_group_players(groups) do :ok -> Events.broadcast() json(conn, StateService.read_state(true)) {:error, :bad_request, message} -> error(conn, :bad_request, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def update_score(conn, %{"stage" => stage, "id" => id} = params) do with {:ok, normalized_stage} <- Stages.validate_stage(stage), {player_id, ""} <- Integer.parse(to_string(id)), score <- parse_score(Map.get(params, "score")), :ok <- EventData.update_score(normalized_stage, player_id, score) do Events.broadcast() json(conn, StateService.read_state(true)) else {:error, message} -> error(conn, :bad_request, message) :error -> error(conn, :bad_request, "invalid player id") {:invalid_score, message} -> error(conn, :bad_request, message) {:error, :bad_request, message} -> error(conn, :bad_request, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def reset_stage(conn, %{"stage" => stage} = params) do with {:ok, target_stages} <- Stages.resolve_reset_stages(stage), reset_proofs <- truthy?(Map.get(params, "resetProofs")), :ok <- EventData.reset_stage_scores(target_stages, reset_proofs) do Events.broadcast() json(conn, StateService.read_state(true)) else {:error, message} -> error(conn, :bad_request, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def update_score_proof(conn, %{"stage" => stage, "id" => id} = params) do with {:ok, normalized_stage} <- Stages.validate_stage(stage), {player_id, ""} <- Integer.parse(to_string(id)), image_data when is_binary(image_data) <- Map.get(params, "imageData"), :ok <- EventData.update_score_proof(normalized_stage, player_id, image_data) do Events.broadcast() json(conn, StateService.read_state(true)) else {:error, message} -> error(conn, :bad_request, message) :error -> error(conn, :bad_request, "invalid player id") nil -> error(conn, :bad_request, "imageData is required") {:error, :bad_request, message} -> error(conn, :bad_request, message) {:error, _, message} -> error(conn, :internal_server_error, message) end end def delete_score_proof(conn, %{"stage" => stage, "id" => id}) do with {:ok, normalized_stage} <- Stages.validate_stage(stage), {player_id, ""} <- Integer.parse(to_string(id)), :ok <- EventData.delete_score_proof(normalized_stage, player_id) do Events.broadcast() json(conn, StateService.read_state(true)) else {:error, message} -> error(conn, :bad_request, message) :error -> error(conn, :bad_request, "invalid player id") {:error, _, message} -> error(conn, :internal_server_error, message) end end def score_advice(conn, %{"stage" => stage, "id" => id}) do with {:ok, normalized_stage} <- Stages.validate_stage(stage), {player_id, ""} <- Integer.parse(to_string(id)), {:ok, image_data} <- EventData.load_score_proof_image(normalized_stage, player_id), {:ok, current_score} <- EventData.load_current_score(normalized_stage, player_id), {:ok, advice} <- Gemini.generate_score_advice(normalized_stage, player_id, image_data, current_score) do json(conn, advice) else {:error, :not_found} -> error(conn, :bad_request, "no proof image found for this score") {:error, :service_unavailable, message} -> error(conn, :service_unavailable, message) {:error, :bad_gateway, message} -> error(conn, :bad_gateway, message) {:error, message} when is_binary(message) -> error(conn, :bad_request, message) :error -> error(conn, :bad_request, "invalid player id") end end defp parse_score(value) do cond do is_integer(value) -> value is_binary(value) -> case Integer.parse(String.trim(value)) do {v, ""} -> v _ -> {:invalid_score, "invalid request body"} end true -> {:invalid_score, "invalid request body"} end end defp truthy?(value) do case value do true -> true 1 -> true "1" -> true "true" -> true "yes" -> true "on" -> true _ -> false end end defp error(conn, status, message) do conn |> put_status(status) |> json(%{"message" => message}) end end