defmodule Towerops.Sites do @moduledoc """ The Sites context. """ import Ecto.Query alias Towerops.Agents.AgentAssignment 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. """ 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 """ Returns the count of sites for an organization. """ 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. """ 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. """ 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. """ 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. """ 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. """ 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. """ def create_site(attrs) do %Site{} |> Site.changeset(attrs) |> Repo.insert() end @doc """ Updates a site. """ def update_site(%Site{} = site, attrs) do old_community = site.snmp_community case site |> Site.changeset(attrs) |> Repo.update() do {:ok, updated_site} = result -> # If community string changed, propagate to inheriting devices if updated_site.snmp_community != old_community do Towerops.Devices.propagate_site_community_change(updated_site.id, updated_site.snmp_community) 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) # Update display_order for all sites new_order |> Enum.with_index(1) |> Enum.each(fn {s, order} -> Repo.update!(Ecto.Changeset.change(s, display_order: order)) end) 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