defmodule Towerops.Sites do
  @moduledoc """
  The Sites context.
  """

  import Ecto.Query

  alias Towerops.Agents.AgentAssignment
  alias Towerops.Contexts.ConfigChangeTracker
  alias Towerops.Devices.Device
  alias Towerops.Repo
  alias Towerops.Sites.Site

  @doc """
  Returns the list of sites for an organization.
  Ordered by custom display_order (if set), then alphabetically by name.
  """
  @spec list_organization_sites(String.t()) :: [Site.t()]
  def list_organization_sites(organization_id) do
    Repo.all(
      from(s in Site,
        where: s.organization_id == ^organization_id,
        order_by: [asc: s.display_order, asc: s.name],
        preload: [:parent_site]
      )
    )
  end

  @doc "Search sites by name for an organization. Returns up to 10 results."
  def search_sites(organization_id, query) do
    search_term = "%#{query}%"

    Site
    |> where(organization_id: ^organization_id)
    |> where([s], ilike(s.name, ^search_term))
    |> order_by(:name)
    |> limit(10)
    |> Repo.all()
  end

  @doc """
  Returns the count of sites for an organization.
  """
  @spec count_organization_sites(String.t()) :: integer()
  def count_organization_sites(organization_id) do
    Repo.aggregate(
      from(s in Site, where: s.organization_id == ^organization_id),
      :count
    )
  end

  @doc """
  Returns the list of root sites (sites without a parent) for an organization.
  Ordered by custom display_order (if set), then alphabetically by name.
  """
  @spec list_root_sites(String.t()) :: [Site.t()]
  def list_root_sites(organization_id) do
    Repo.all(
      from(s in Site,
        where: s.organization_id == ^organization_id,
        where: is_nil(s.parent_site_id),
        order_by: [asc: s.display_order, asc: s.name]
      )
    )
  end

  @doc """
  Returns the list of child sites for a parent site.
  Ordered by custom display_order (if set), then alphabetically by name.
  """
  @spec list_child_sites(String.t()) :: [Site.t()]
  def list_child_sites(parent_site_id) do
    Repo.all(
      from(s in Site,
        where: s.parent_site_id == ^parent_site_id,
        order_by: [asc: s.display_order, asc: s.name]
      )
    )
  end

  @doc """
  Gets a single site.
  """
  @spec get_site!(String.t()) :: Site.t()
  def get_site!(id) do
    Site
    |> Repo.get!(id)
    |> Repo.preload([:parent_site, :child_sites, :device])
  end

  @doc """
  Gets a single site. Returns nil if not found.
  """
  @spec get_site(String.t()) :: Site.t() | nil
  def get_site(id) do
    case Repo.get(Site, id) do
      nil -> nil
      site -> Repo.preload(site, [:parent_site, :child_sites, :device])
    end
  end

  @doc """
  Gets a single site belonging to an organization.
  """
  @spec get_organization_site!(String.t(), String.t()) :: Site.t()
  def get_organization_site!(organization_id, site_id) do
    Repo.one!(
      from(s in Site,
        where: s.id == ^site_id,
        where: s.organization_id == ^organization_id,
        preload: [:parent_site, :child_sites, :device]
      )
    )
  end

  @doc """
  Creates a site.
  """
  @spec create_site(map()) :: {:ok, Site.t()} | {:error, Ecto.Changeset.t()}
  def create_site(attrs) do
    %Site{}
    |> Site.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a site.
  """
  def update_site(%Site{} = site, attrs) do
    old_config = ConfigChangeTracker.capture_config_state(site)

    case site
         |> Site.changeset(attrs)
         |> Repo.update() do
      {:ok, updated_site} = result ->
        # If community string changed, propagate to inheriting devices
        if ConfigChangeTracker.snmp_changed?(updated_site, old_config) do
          Towerops.Devices.propagate_site_community_change(updated_site.id, updated_site.snmp_community)
        end

        # If any MikroTik settings changed, propagate to inheriting devices
        if ConfigChangeTracker.mikrotik_changed?(updated_site, old_config) do
          mikrotik_attrs = ConfigChangeTracker.extract_mikrotik_attrs(updated_site)
          Towerops.Devices.propagate_site_mikrotik_change(updated_site.id, mikrotik_attrs)
        end

        result

      error ->
        error
    end
  end

  @doc """
  Deletes a site.
  """
  def delete_site(%Site{} = site) do
    Repo.delete(site)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking site changes.
  """
  def change_site(%Site{} = site, attrs \\ %{}) do
    Site.changeset(site, attrs)
  end

  @doc """
  Builds a hierarchical tree structure of sites.
  """
  def build_site_tree(organization_id) do
    sites = list_organization_sites(organization_id)
    root_sites = Enum.filter(sites, &is_nil(&1.parent_site_id))

    Enum.map(root_sites, fn root ->
      build_tree_node(root, sites)
    end)
  end

  defp build_tree_node(site, all_sites) do
    children =
      all_sites
      |> Enum.filter(&(&1.parent_site_id == site.id))
      |> Enum.map(&build_tree_node(&1, all_sites))

    %{site: site, children: children}
  end

  ## Bulk Configuration Updates

  @doc """
  Applies site's SNMP configuration to all devices at the site.

  This updates all devices records to use the site's SNMP version and community string,
  overwriting any existing device-level configurations.

  Returns the number of device records updated.
  """
  def apply_snmp_config_to_all_equipment(site_id) do
    site = get_site!(site_id)

    Repo.update_all(from(e in Device, where: e.site_id == ^site_id),
      set: [
        snmp_version: site.snmp_version,
        snmp_community: site.snmp_community,
        updated_at: DateTime.truncate(DateTime.utc_now(), :second)
      ]
    )
  end

  @doc """
  Applies site's agent to all devices at the site.

  This creates/updates agent assignments for all devices at the site to use the site's
  agent, overwriting any existing device-level assignments.

  Returns the number of device records assigned.
  """
  def apply_agent_to_all_equipment(site_id) do
    site = Repo.get!(Site, site_id)

    # device IDs for this site
    device_ids = Repo.all(from(e in Device, where: e.site_id == ^site_id, select: e.id))

    if site.agent_token_id do
      # Delete existing assignments
      Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))

      # Create new assignments
      now = DateTime.truncate(DateTime.utc_now(), :second)

      assignments =
        Enum.map(device_ids, fn device_id ->
          %{
            device_id: device_id,
            agent_token_id: site.agent_token_id,
            inserted_at: now,
            updated_at: now
          }
        end)

      {count, _} = Repo.insert_all(AgentAssignment, assignments)
      {count, nil}
    else
      # No agent set, delete all assignments (they will inherit from org or use cloud)
      Repo.delete_all(from(a in AgentAssignment, where: a.device_id in ^device_ids))
    end
  end

  ## Display Order Management

  @doc """
  Reorders a site to a new position within the organization.

  Updates display_order for all sites to maintain continuous ordering (1, 2, 3, ...).
  The new_position is 1-based (1 = first position).

  Returns {:ok, site} on success, {:error, changeset} on failure.
  """
  def reorder_site(site_id, new_position) when is_integer(new_position) and new_position > 0 do
    Repo.transaction(fn ->
      site = Repo.get!(Site, site_id)
      organization_id = site.organization_id

      # Get all sites excluding the one being moved, in current order
      other_sites =
        Repo.all(
          from(s in Site,
            where: s.organization_id == ^organization_id and s.id != ^site_id,
            order_by: [asc: s.display_order, asc: s.name]
          )
        )

      # Insert at new position (1-based index, convert to 0-based for List.insert_at)
      new_order = List.insert_at(other_sites, new_position - 1, site)

      {site_ids, orders} =
        new_order
        |> Enum.with_index(1)
        |> Enum.map(fn {s, order} -> {s.id, order} end)
        |> Enum.unzip()

      site_ids = Enum.map(site_ids, &Ecto.UUID.dump!/1)
      now = DateTime.truncate(DateTime.utc_now(), :second)

      query =
        from(s in Site,
          join:
            data in fragment(
              "SELECT * FROM unnest(?::uuid[], ?::int[]) AS data(id, ordering)",
              ^site_ids,
              ^orders
            ),
          on: s.id == data.id,
          update: [set: [display_order: field(data, :ordering), updated_at: ^now]]
        )

      Repo.update_all(query, [])

      Repo.get!(Site, site_id)
    end)
  end

  @doc """
  Reset all sites in organization to alphabetical order.

  Clears display_order field for all sites, allowing them to fall back to alphabetical sorting.
  """
  def reset_site_order(organization_id) do
    Repo.update_all(
      from(s in Site, where: s.organization_id == ^organization_id),
      set: [display_order: nil, updated_at: DateTime.truncate(DateTime.utc_now(), :second)]
    )
  end
end
