defmodule Towerops.Preseem.Sync do
  @moduledoc """
  Orchestrates syncing data from the Preseem API into the local database.

  Pulls access point scores from Preseem, upserts them
  into `preseem_access_points`, and logs each sync operation.
  """

  alias Towerops.Integrations
  alias Towerops.Preseem.AccessPoint
  alias Towerops.Preseem.Client
  alias Towerops.Preseem.DeviceMatcher
  alias Towerops.Preseem.SyncLog
  alias Towerops.Repo

  require Logger

  @doc """
  Main entry point: syncs all access points for the given integration.

  Fetches AP scores from the Preseem API, upserts them, writes a sync log,
  and updates the integration's sync status.

  Returns `{:ok, %{synced: count, matched: count}}` or `{:error, reason}`.
  """
  def sync_organization(%Integrations.Integration{} = integration) do
    start_time = System.monotonic_time(:millisecond)
    api_key = integration.credentials["api_key"]
    org_id = integration.organization_id

    case Client.list_access_points(api_key) do
      {:ok, ap_list} ->
        handle_successful_sync(integration, org_id, ap_list, start_time)

      {:error, reason} ->
        handle_failed_sync(integration, reason, start_time)
    end
  end

  @doc """
  Upserts a single access point from raw API response data.

  Maps Preseem scores API fields to schema fields and uses the unique constraint on
  `[:organization_id, :preseem_id]` for conflict resolution.
  """
  def upsert_access_point(organization_id, ap_data) do
    attrs = %{
      organization_id: organization_id,
      preseem_id: ap_data["element_uuid"],
      name: ap_data["name"],
      mac_address: ap_data["system_mac_address"],
      ip_address: ap_data["management_ip"],
      model: ap_data["model_name"],
      firmware: ap_data["firmware"],
      capacity_score: cast_to_float(ap_data["subscriber_capacity"]),
      qoe_score: cast_to_float(ap_data["ap_health_score"]),
      rf_score: cast_to_float(ap_data["rf_score"]),
      busy_hours: nil,
      airtime_utilization: cast_to_float(ap_data["airtime_remaining_tx"]),
      subscriber_count: ap_data["connections"],
      raw_data: ap_data
    }

    %AccessPoint{}
    |> AccessPoint.changeset(attrs)
    |> Repo.insert(
      on_conflict:
        {:replace_all_except, [:id, :organization_id, :preseem_id, :device_id, :match_confidence, :inserted_at]},
      conflict_target: [:organization_id, :preseem_id],
      returning: true
    )
  end

  @doc """
  Inserts a subscriber metric record for the given access point.
  """
  def insert_metrics(access_point_id, metrics_data) do
    alias Towerops.Preseem.SubscriberMetric

    recorded_at =
      case metrics_data["recorded_at"] do
        nil -> DateTime.truncate(DateTime.utc_now(), :second)
        dt_string -> parse_datetime(dt_string)
      end

    attrs = %{
      preseem_access_point_id: access_point_id,
      avg_latency: metrics_data["avg_latency"],
      avg_jitter: metrics_data["avg_jitter"],
      avg_loss: metrics_data["avg_loss"],
      avg_throughput: metrics_data["avg_throughput"],
      p95_latency: metrics_data["p95_latency"],
      subscriber_count: metrics_data["subscriber_count"],
      recorded_at: recorded_at
    }

    %SubscriberMetric{}
    |> SubscriberMetric.changeset(attrs)
    |> Repo.insert()
  end

  defp handle_successful_sync(integration, org_id, ap_list, start_time) do
    synced_count =
      Enum.reduce(ap_list, 0, fn ap_data, count ->
        case upsert_access_point(org_id, ap_data) do
          {:ok, _ap} ->
            count + 1

          {:error, changeset} ->
            Logger.warning("Failed to upsert Preseem AP: #{inspect(changeset.errors)}")
            count
        end
      end)

    matched_count = run_device_matching(org_id)
    duration_ms = System.monotonic_time(:millisecond) - start_time

    status = if synced_count == length(ap_list), do: "success", else: "partial"

    log_sync(integration, status, synced_count, %{}, duration_ms)
    Integrations.update_sync_status(integration, status)

    {:ok, %{synced: synced_count, matched: matched_count}}
  end

  defp handle_failed_sync(integration, reason, start_time) do
    duration_ms = System.monotonic_time(:millisecond) - start_time

    log_sync(integration, "failed", 0, %{error: inspect(reason)}, duration_ms)
    Integrations.update_sync_status(integration, "failed")

    {:error, reason}
  end

  defp run_device_matching(org_id) do
    if Code.ensure_loaded?(DeviceMatcher) do
      case DeviceMatcher.match_unmatched(org_id) do
        {:ok, count} -> count
        _ -> 0
      end
    else
      0
    end
  end

  defp log_sync(integration, status, records_synced, errors, duration_ms) do
    %SyncLog{}
    |> SyncLog.changeset(%{
      organization_id: integration.organization_id,
      integration_id: integration.id,
      status: status,
      records_synced: records_synced,
      errors: errors,
      duration_ms: duration_ms,
      inserted_at: DateTime.truncate(DateTime.utc_now(), :second)
    })
    |> Repo.insert()
  end

  defp parse_datetime(dt_string) when is_binary(dt_string) do
    case DateTime.from_iso8601(dt_string) do
      {:ok, dt, _offset} -> DateTime.truncate(dt, :second)
      _ -> DateTime.truncate(DateTime.utc_now(), :second)
    end
  end

  defp cast_to_float(nil), do: nil
  defp cast_to_float(val) when is_float(val), do: val
  defp cast_to_float(val) when is_integer(val), do: val / 1
end
