defmodule Towerops.Workers.CheckWorker do
  @moduledoc """
  Oban worker that executes service checks (HTTP, TCP, DNS).

  Follows same self-scheduling pattern as DeviceMonitorWorker.
  Phoenix executes checks unless assigned to an agent (agent_token_id set).
  """
  use Oban.Worker,
    queue: :checks,
    unique: [
      period: 60,
      keys: [:check_id],
      states: [:available, :scheduled, :executing, :retryable]
    ]

  alias Towerops.Agents
  alias Towerops.Alerts
  alias Towerops.Monitoring
  alias Towerops.Monitoring.Executor
  alias Towerops.Snmp.Client
  alias Towerops.Workers.PollingOffset

  require Logger

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"check_id" => check_id}}) do
    case Monitoring.get_check(check_id) do
      nil ->
        Logger.debug("Check #{check_id} no longer exists, skipping and not rescheduling")
        :ok

      check ->
        if should_continue_checking?(check) do
          maybe_perform_check(check)
          schedule_next_check_with_error_handling(check_id, check.interval_seconds)
        else
          Logger.debug("Check #{check_id} disabled or assigned to agent, not rescheduling",
            check_id: check_id,
            enabled: check.enabled,
            agent_assigned: !is_nil(check.agent_token_id)
          )
        end

        :ok
    end
  end

  # Check should continue if:
  # - Enabled
  # - Not a passive check (passive checks don't execute actively)
  # - Either no agent assigned OR agent is cloud poller (Phoenix executes)
  defp should_continue_checking?(check) do
    check.enabled &&
      check.check_type != "passive" &&
      !Client.phoenix_snmp_disabled() &&
      should_phoenix_execute?(check)
  end

  defp should_phoenix_execute?(check) do
    case check.agent_token_id do
      nil ->
        # No agent assigned - Phoenix executes
        true

      agent_token_id ->
        # Agent assigned - only Phoenix executes if it's cloud poller
        try do
          agent_token = Agents.get_agent_token!(agent_token_id)
          agent_token.is_cloud_poller
        rescue
          Ecto.NoResultsError -> true
        end
    end
  end

  defp maybe_perform_check(check) do
    if should_phoenix_execute?(check) do
      perform_check(check)
    else
      Logger.debug("Skipping Phoenix execution for check #{check.name} - assigned to remote agent")
      :ok
    end
  end

  defp schedule_next_check_with_error_handling(check_id, interval_seconds) do
    case Monitoring.get_check(check_id) do
      nil ->
        Logger.debug("Check deleted, not rescheduling", check_id: check_id)
        :ok

      _check ->
        case schedule_next_check(check_id, interval_seconds) do
          {:ok, _job} ->
            :ok

          {:error, changeset} ->
            Logger.error("Failed to schedule next check for #{check_id}: #{inspect(changeset.errors)}")
            :ok
        end
    end
  end

  @doc """
  Starts executing a check (schedules first execution).
  """
  def start_check(check_id, interval_seconds \\ 60) do
    offset = PollingOffset.calculate_offset(check_id, interval_seconds)

    %{check_id: check_id}
    |> new(schedule_in: offset)
    |> Oban.insert()
  end

  @doc """
  Stops executing a check by cancelling its jobs.
  """
  def stop_check(check_id) do
    import Ecto.Query

    Oban.cancel_all_jobs(
      from(j in Oban.Job,
        where: j.worker == "Towerops.Workers.CheckWorker",
        where: fragment("args->>'check_id' = ?", ^check_id),
        where: j.state in ["available", "scheduled", "executing", "retryable"]
      )
    )
  end

  @doc """
  Triggers an immediate check execution.
  """
  def trigger_check(check_id) do
    %{check_id: check_id}
    |> new()
    |> Oban.insert()
  end

  # Private Functions

  defp perform_check(check) do
    Logger.debug("Executing check: #{check.name} (#{check.check_type})", check_id: check.id)

    # Execute the check
    result = Executor.execute(check)

    # Convert result to status and extract details
    {status, response_time, output} =
      case result do
        {:ok, time, msg} -> {0, time, msg}
        {:error, reason} -> {2, nil, reason}
      end

    # Record the result
    now = DateTime.utc_now()

    case Monitoring.create_check_result(%{
           check_id: check.id,
           organization_id: check.organization_id,
           status: status,
           output: output,
           response_time_ms: response_time,
           checked_at: now
         }) do
      {:ok, _result} ->
        Logger.debug("Recorded check result: #{output}", check_id: check.id, status: status)

      {:error, changeset} ->
        Logger.error("Failed to create check result: #{inspect(changeset.errors)}")
    end

    # Update check state (handles soft/hard state transitions)
    old_state = check.current_state
    {:ok, updated_check} = Monitoring.update_check_state(check, status, output)

    # Handle state changes (create/resolve alerts)
    if state_changed_to_problem?(old_state, updated_check) do
      handle_check_problem(updated_check, output)
    end

    if state_changed_to_ok?(old_state, updated_check) do
      handle_check_recovery(updated_check)
    end

    :ok
  end

  defp state_changed_to_problem?(old_state, check) do
    # State changed from OK to CRITICAL/WARNING and is now hard state
    old_state == 0 && check.current_state in [1, 2] && check.current_state_type == "hard"
  end

  defp state_changed_to_ok?(old_state, check) do
    # State changed from CRITICAL/WARNING to OK
    old_state in [1, 2] && check.current_state == 0
  end

  defp handle_check_problem(check, output) do
    Logger.warning("Check #{check.name} entered problem state",
      check_id: check.id,
      state: check.current_state,
      output: output
    )

    # Create alert if one doesn't exist
    if !Alerts.has_active_check_alert?(check.id) do
      create_check_alert(check, output)
    end
  end

  defp handle_check_recovery(check) do
    Logger.info("Check #{check.name} recovered",
      check_id: check.id
    )

    # Resolve any active alerts
    Alerts.resolve_check_alerts(check.id)
  end

  defp create_check_alert(check, output) do
    severity = if check.current_state == 1, do: 1, else: 2

    attrs = %{
      check_id: check.id,
      device_id: check.device_id,
      organization_id: check.organization_id,
      alert_type: "check_#{check.check_type}",
      severity: severity,
      message: "Check '#{check.name}' is #{Monitoring.Check.state_label(check.current_state)}",
      output: output,
      triggered_at: DateTime.utc_now()
    }

    case Alerts.create_alert(attrs) do
      {:ok, alert} ->
        Logger.info("Created alert for check #{check.name}", alert_id: alert.id)

      {:error, changeset} ->
        Logger.error("Failed to create alert: #{inspect(changeset.errors)}")
    end
  end

  defp schedule_next_check(check_id, interval_seconds) do
    offset = PollingOffset.calculate_offset(check_id, interval_seconds)

    %{check_id: check_id}
    |> new(schedule_in: offset)
    |> Oban.insert()
  end
end
