defmodule ToweropsWeb.Api.V1.SitesController do
  @moduledoc """
  API controller for managing sites.

  All endpoints require API token authentication and operations are scoped
  to the organization associated with the token.
  """
  use ToweropsWeb, :controller

  alias Towerops.Sites

  @doc """
  GET /api/v1/sites

  Lists all sites for the authenticated organization.

  Response:
  {
    "sites": [
      {
        "id": "uuid",
        "name": "Main Office",
        "location": "New York, NY",
        "snmp_community": "public",
        "inserted_at": "2026-01-15T19:44:25Z"
      }
    ]
  }
  """
  def index(conn, _params) do
    organization_id = conn.assigns.current_organization_id

    sites =
      organization_id
      |> Sites.list_organization_sites()
      |> Enum.map(&format_site/1)

    json(conn, %{sites: sites})
  end

  @doc """
  POST /api/v1/sites

  Creates a new site for the authenticated organization.

  Request body:
  {
    "site": {
      "name": "Main Office",
      "location": "New York, NY",
      "snmp_community": "public"  # optional
    }
  }

  Response:
  {
    "id": "uuid",
    "name": "Main Office",
    "location": "New York, NY",
    "snmp_community": "public",
    "inserted_at": "2026-01-15T19:44:25Z"
  }
  """
  def create(conn, %{"site" => site_params}) do
    organization_id = conn.assigns.current_organization_id
    attrs = Map.put(site_params, "organization_id", organization_id)

    case Sites.create_site(attrs) do
      {:ok, site} ->
        conn
        |> put_status(:created)
        |> json(format_site(site))

      {:error, %Ecto.Changeset{} = changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{errors: translate_errors(changeset)})
    end
  end

  def create(conn, _params) do
    conn
    |> put_status(:bad_request)
    |> json(%{error: "Missing 'site' parameter"})
  end

  @doc """
  GET /api/v1/sites/:id

  Gets a single site by ID.

  Response:
  {
    "id": "uuid",
    "name": "Main Office",
    "location": "New York, NY",
    "snmp_community": "public",
    "inserted_at": "2026-01-15T19:44:25Z"
  }
  """
  def show(conn, %{"id" => id}) do
    organization_id = conn.assigns.current_organization_id
    site = Sites.get_site!(id)

    if site.organization_id == organization_id do
      json(conn, format_site(site))
    else
      conn
      |> put_status(:forbidden)
      |> json(%{error: "Access denied to this site"})
    end
  rescue
    Ecto.NoResultsError ->
      conn
      |> put_status(:not_found)
      |> json(%{error: "Site not found"})
  end

  @doc """
  PATCH /api/v1/sites/:id

  Updates a site.

  Request body:
  {
    "site": {
      "name": "Updated Name",
      "location": "Boston, MA"
    }
  }

  Response:
  {
    "id": "uuid",
    "name": "Updated Name",
    "location": "Boston, MA",
    "snmp_community": "public",
    "inserted_at": "2026-01-15T19:44:25Z"
  }
  """
  def update(conn, %{"id" => id, "site" => site_params}) do
    organization_id = conn.assigns.current_organization_id
    site = Sites.get_site!(id)

    if site.organization_id == organization_id do
      case Sites.update_site(site, site_params) do
        {:ok, updated_site} ->
          json(conn, format_site(updated_site))

        {:error, %Ecto.Changeset{} = changeset} ->
          conn
          |> put_status(:unprocessable_entity)
          |> json(%{errors: translate_errors(changeset)})
      end
    else
      conn
      |> put_status(:forbidden)
      |> json(%{error: "Access denied to this site"})
    end
  rescue
    Ecto.NoResultsError ->
      conn
      |> put_status(:not_found)
      |> json(%{error: "Site not found"})
  end

  def update(conn, _params) do
    conn
    |> put_status(:bad_request)
    |> json(%{error: "Missing 'site' parameter"})
  end

  @doc """
  DELETE /api/v1/sites/:id

  Deletes a site.

  Response:
  {
    "success": true
  }
  """
  def delete(conn, %{"id" => id}) do
    organization_id = conn.assigns.current_organization_id
    site = Sites.get_site!(id)

    if site.organization_id == organization_id do
      case Sites.delete_site(site) do
        {:ok, _site} ->
          json(conn, %{success: true})

        {:error, %Ecto.Changeset{} = changeset} ->
          conn
          |> put_status(:unprocessable_entity)
          |> json(%{errors: translate_errors(changeset)})
      end
    else
      conn
      |> put_status(:forbidden)
      |> json(%{error: "Access denied to this site"})
    end
  rescue
    Ecto.NoResultsError ->
      conn
      |> put_status(:not_found)
      |> json(%{error: "Site not found"})
  end

  # Private helpers

  defp format_site(site) do
    %{
      id: site.id,
      name: site.name,
      location: site.location,
      snmp_community: site.snmp_community,
      inserted_at: site.inserted_at
    }
  end

  defp translate_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
        safe_translate_key(key, opts)
      end)
    end)
  end

  # Safely translate error message keys to prevent atom exhaustion
  defp safe_translate_key(key, opts) do
    # Validate key length and format before converting to atom to prevent abuse
    if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
      opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
    else
      key
    end
  end
end
