defmodule Towerops.Gaiia.Reconciliation do
  @moduledoc """
  Inventory reconciliation engine comparing Gaiia inventory items
  against Towerops device discovery.

  Detects four types of discrepancies:
  1. Ghost devices — in Gaiia but not discovered by Towerops
  2. Untracked devices — discovered by Towerops but not in Gaiia
  3. Data mismatches — both systems have the device but fields disagree
  4. Missing mappings — Gaiia items not yet linked to any Towerops device
  """

  import Ecto.Query

  alias Towerops.Devices
  alias Towerops.Gaiia.InventoryItem
  alias Towerops.Repo

  @doc """
  Run a full reconciliation for an organization.

  Returns a map with:
  - `:ghost_devices` — Gaiia items mapped to devices that no longer exist
  - `:untracked_devices` — Towerops devices with no matching Gaiia item
  - `:data_mismatches` — mapped pairs where fields disagree (IP, serial, etc.)
  - `:missing_mappings` — Gaiia items with no device mapping
  - `:summary` — aggregate counts
  - `:reconciled_at` — timestamp
  """
  def reconcile(organization_id) do
    inventory_items = list_inventory_items(organization_id)
    devices = Devices.list_organization_devices(organization_id)

    {mapped, unmapped_items} = partition_items(inventory_items)
    mapped_device_ids = MapSet.new(mapped, fn {_item, device_id} -> device_id end)

    devices_by_id = Map.new(devices, &{&1.id, &1})

    ghost_devices = find_ghost_devices(mapped, devices_by_id)
    untracked_devices = find_untracked_devices(devices, mapped_device_ids)
    data_mismatches = find_data_mismatches(mapped, devices_by_id)
    missing_mappings = build_missing_mappings(unmapped_items)

    total_mapped = length(mapped) - length(ghost_devices)

    %{
      ghost_devices: ghost_devices,
      untracked_devices: untracked_devices,
      data_mismatches: data_mismatches,
      missing_mappings: missing_mappings,
      summary: %{
        total_mapped: total_mapped,
        ghost_count: length(ghost_devices),
        untracked_count: length(untracked_devices),
        mismatch_count: length(data_mismatches),
        missing_mapping_count: length(missing_mappings)
      },
      reconciled_at: DateTime.truncate(DateTime.utc_now(), :second)
    }
  end

  defp list_inventory_items(organization_id) do
    InventoryItem
    |> where(organization_id: ^organization_id)
    |> Repo.all()
  end

  defp partition_items(items) do
    {mapped, unmapped} =
      Enum.split_with(items, fn item -> not is_nil(item.device_id) end)

    mapped_pairs = Enum.map(mapped, fn item -> {item, item.device_id} end)
    {mapped_pairs, unmapped}
  end

  # Gaiia items mapped to devices that no longer exist in Towerops
  defp find_ghost_devices(mapped, devices_by_id) do
    mapped
    |> Enum.filter(fn {_item, device_id} ->
      not Map.has_key?(devices_by_id, device_id)
    end)
    |> Enum.map(fn {item, _device_id} ->
      %{inventory_item: item, type: :ghost}
    end)
  end

  # Towerops devices with no Gaiia inventory item mapped
  defp find_untracked_devices(devices, mapped_device_ids) do
    devices
    |> Enum.reject(fn device -> MapSet.member?(mapped_device_ids, device.id) end)
    |> Enum.map(fn device -> %{device: device, type: :untracked} end)
  end

  # Mapped pairs where comparable fields disagree
  defp find_data_mismatches(mapped, devices_by_id) do
    Enum.flat_map(mapped, fn {item, device_id} ->
      case Map.get(devices_by_id, device_id) do
        nil -> []
        device -> compare_fields(item, device)
      end
    end)
  end

  defp compare_fields(item, device) do
    maybe_add_mismatch([], :ip_address, to_string_or_nil(device.ip_address), item.ip_address, item, device)
  end

  defp maybe_add_mismatch(acc, _field, nil, _gaiia, _item, _device), do: acc
  defp maybe_add_mismatch(acc, _field, _towerops, nil, _item, _device), do: acc

  defp maybe_add_mismatch(acc, field, towerops_val, gaiia_val, item, device) do
    if normalize_value(towerops_val) == normalize_value(gaiia_val) do
      acc
    else
      [
        %{
          field: field,
          towerops_value: towerops_val,
          gaiia_value: gaiia_val,
          inventory_item: item,
          device: device
        }
        | acc
      ]
    end
  end

  defp normalize_value(val) when is_binary(val), do: String.downcase(String.trim(val))
  defp normalize_value(val), do: val

  defp to_string_or_nil(nil), do: nil
  defp to_string_or_nil(%{address: addr}) when is_tuple(addr), do: addr |> :inet.ntoa() |> to_string()
  defp to_string_or_nil(val) when is_binary(val), do: val
  defp to_string_or_nil(val), do: to_string(val)

  defp build_missing_mappings(unmapped_items) do
    Enum.map(unmapped_items, fn item ->
      %{inventory_item: item, type: :missing_mapping}
    end)
  end
end
