defmodule Aprsme.Is do
  @moduledoc false
  use GenServer

  alias Aprsme.Is.LoginParams
  alias Aprsme.Is.PacketStats

  require Logger

  @aprs_timeout 60 * 1000
  @keepalive_interval 20 * 1000
  @backpressure_safety_valve_timeout 30_000
  @fallback_server "noam.aprs2.net"
  @fallback_timeout_seconds 300

  defstruct [
    :server,
    :port,
    :socket,
    :timer,
    :keepalive_timer,
    :connected_at,
    :packet_stats,
    :login_params,
    :safety_valve_timer,
    :failure_started_at,
    buffer: "",
    backpressure_active: false
  ]

  @type t :: %__MODULE__{
          server: String.t(),
          port: pos_integer(),
          socket: :ssl.sslsocket() | nil,
          timer: reference() | nil,
          keepalive_timer: reference() | nil,
          connected_at: DateTime.t() | nil,
          packet_stats: PacketStats.t(),
          buffer: String.t(),
          login_params: LoginParams.t(),
          backpressure_active: boolean(),
          safety_valve_timer: reference() | nil,
          failure_started_at: DateTime.t() | nil
        }

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    env = Application.get_env(:aprsme, :env)
    disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)

    do_init(env, disable_connection)
  end

  defp do_init(:test, _) do
    Logger.info("APRS-IS connection disabled in test environment")
    {:stop, :test_environment_disabled}
  end

  defp do_init(_, true) do
    Logger.warning("APRS-IS connection explicitly disabled")
    {:stop, :test_environment_disabled}
  end

  defp do_init(_env, false) do
    # Trap exits so we can gracefully shut down
    Process.flag(:trap_exit, true)

    # Add a small delay to prevent rapid reconnection attempts
    Process.sleep(Application.get_env(:aprsme, :is_init_delay_ms, 2_000))

    # Get startup parameters
    server = initial_server()

    # If we fell back to the fallback server in a previous failure streak and
    # the GenServer restarted (e.g. due to timeout), persist the fallback choice.
    server =
      if Application.get_env(:aprsme, :aprs_is_using_fallback, false) do
        Logger.warning("APRS-IS fallback active from previous failure, connecting to #{@fallback_server}")
        @fallback_server
      else
        server
      end

    port = Application.get_env(:aprsme, :aprs_is_port, 14_580)
    default_filter = Application.get_env(:aprsme, :aprs_is_default_filter, "r/33/-96/100")
    aprs_user_id = Application.get_env(:aprsme, :aprs_is_login_id, "W5ISP")
    aprs_passcode = Application.get_env(:aprsme, :aprs_is_password, "-1")

    # Record connection start time
    connected_at = DateTime.utc_now()

    # Initialize packet statistics
    packet_stats = %PacketStats{last_second_timestamp: System.system_time(:second)}

    # Initialize state without requiring immediate connection
    state = %__MODULE__{
      server: server,
      port: port,
      connected_at: connected_at,
      packet_stats: packet_stats,
      login_params: %LoginParams{
        user_id: aprs_user_id,
        passcode: aprs_passcode,
        filter: default_filter
      },
      failure_started_at: nil
    }

    # Try to connect initially, but don't fail if it doesn't work
    case connect_to_aprs_is(server, port) do
      {:ok, socket} ->
        case send_login_string(socket, aprs_user_id, aprs_passcode, default_filter) do
          :ok ->
            timer = create_timer(@aprs_timeout)
            keepalive_timer = create_keepalive_timer(@keepalive_interval)
            {:ok, reset_failure(%{state | socket: socket, timer: timer, keepalive_timer: keepalive_timer})}

          error ->
            Logger.warning("Failed to login to APRS-IS: #{inspect(error)}, will retry")
            :gen_tcp.close(socket)
            schedule_reconnect(5000)
            {:ok, track_failure(state)}
        end

      error ->
        Logger.warning("Unable to establish initial connection to APRS-IS: #{inspect(error)}, will retry")
        schedule_reconnect(5000)
        {:ok, track_failure(state)}
    end
  end

  defp initial_server do
    server_raw = Application.get_env(:aprsme, :aprs_is_server, ~c"dallas.aprs2.net")
    to_server_string(server_raw)
  end

  # Client API

  def stop do
    Logger.info("Stopping Server")
    GenServer.stop(__MODULE__, :stop)
  end

  def get_status do
    status_from(Process.whereis(__MODULE__))
  end

  # GenServer isn't running — return a disconnected snapshot.
  defp status_from(nil), do: disconnected_status(nil)

  defp status_from(pid) when is_pid(pid) do
    GenServer.call(__MODULE__, :get_status, Application.get_env(:aprsme, :is_call_timeout_ms, 5_000))
  catch
    # GenServer exists but isn't responding — treat as disconnected but
    # fall back to the rotate.aprs2.net default server string.
    :exit, _ -> disconnected_status(~c"rotate.aprs2.net")
  end

  # Build the "I'm not connected" status map. `default_server` is only used
  # when the application config has no server set; pass nil to mean "use
  # whatever is in config, or nil".
  defp disconnected_status(default_server) do
    server = Application.get_env(:aprsme, :aprs_is_server, default_server)
    port = Application.get_env(:aprsme, :aprs_is_port, 14_580)
    {stored_packet_count, oldest_packet_timestamp} = safe_packet_storage_stats()

    %{
      connected: false,
      server: server_to_string(server),
      port: port,
      connected_at: nil,
      uptime_seconds: 0,
      login_id: Application.get_env(:aprsme, :aprs_is_login_id, "W5ISP"),
      filter: Application.get_env(:aprsme, :aprs_is_default_filter, "r/33/-96/100"),
      packet_stats: default_packet_stats(),
      stored_packet_count: stored_packet_count,
      oldest_packet_timestamp: oldest_packet_timestamp
    }
  end

  defp safe_packet_storage_stats do
    {Aprsme.Packets.get_total_packet_count(), Aprsme.Packets.get_oldest_packet_timestamp()}
  rescue
    DBConnection.OwnershipError ->
      {0, nil}
  catch
    :exit, _ ->
      {0, nil}
  end

  def set_filter(filter_string), do: send_message("#filter #{filter_string}")
  def list_active_filters, do: send_message("#filter?")

  def send_message(from, to, message) do
    padded_callsign = String.pad_trailing(to, 9)
    send_message("#{from}>APRS,TCPIP*::#{padded_callsign}:#{message}")
  end

  def send_message(message) do
    case Process.whereis(__MODULE__) do
      nil -> {:error, :not_connected}
      _pid -> GenServer.call(__MODULE__, {:send_message, message})
    end
  end

  # Server methods

  @spec connect_to_aprs_is(String.t() | charlist(), pos_integer()) ::
          {:ok, :ssl.sslsocket()} | {:error, any()}
  defp connect_to_aprs_is(server, port) do
    # Additional safeguard: prevent connections in test environment
    env = Application.get_env(:aprsme, :env)
    disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false)

    do_connect_to_aprs_is(server, port, env, disable_connection)
  end

  defp do_connect_to_aprs_is(_server, _port, :test, _) do
    Logger.warning("Attempted APRS-IS connection blocked in test environment")
    {:error, :test_environment_blocked}
  end

  defp do_connect_to_aprs_is(_server, _port, _, true) do
    Logger.warning("Attempted APRS-IS connection blocked in test environment")
    {:error, :test_environment_blocked}
  end

  defp do_connect_to_aprs_is(server, port, _env, false) do
    Logger.debug("Connecting to: #{server}:#{port}")
    opts = [:binary, active: true]
    server_string = to_server_string(server)
    :gen_tcp.connect(String.to_charlist(server_string), port, opts)
  end

  defp to_server_string(server) when is_list(server), do: List.to_string(server)
  defp to_server_string(server) when is_binary(server), do: server

  @spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) ::
          :ok | {:error, any()}
  defp send_login_string(socket, aprs_user_id, aprs_passcode, filter) do
    login_string =
      "user #{aprs_user_id} pass #{aprs_passcode} vers aprs.me 0.1 filter #{filter}\r\n"

    Logger.info("Sending login string: user #{aprs_user_id} pass ***** vers aprs.me 0.1 filter #{filter}")

    :gen_tcp.send(socket, login_string)
  end

  @spec create_timer(non_neg_integer()) :: reference()
  defp create_timer(timeout) do
    Process.send_after(self(), :aprsme_no_message_timeout, timeout)
  end

  @spec create_keepalive_timer(non_neg_integer()) :: reference()
  defp create_keepalive_timer(interval) do
    Process.send_after(self(), :send_keepalive, interval)
  end

  @impl true
  def handle_call({:send_message, message}, _from, state) do
    case state.socket do
      nil ->
        Logger.warning("Attempted to send message while not connected to APRS-IS")
        {:reply, {:error, :not_connected}, state}

      socket ->
        next_ack_number = :ets.update_counter(:aprsme, :message_number, 1)
        # Append ack number
        message = message <> "{" <> to_string(next_ack_number) <> "\r"

        Logger.info("Sending message: #{inspect(message)}")

        case :gen_tcp.send(socket, message) do
          :ok ->
            {:reply, :ok, state}

          error ->
            Logger.error("Failed to send message to APRS-IS: #{inspect(error)}")
            cancel_timer(state.timer)
            cancel_keepalive_timer(state.keepalive_timer)
            :gen_tcp.close(socket)
            schedule_reconnect(5000)

            state = track_failure(%{state | socket: nil, timer: nil, keepalive_timer: nil})

            {:reply, {:error, error}, state}
        end
    end
  end

  @impl true
  def handle_call(:get_status, _from, state) do
    connected = state.socket != nil

    status = %{
      connected: connected,
      server: server_to_string(state.server),
      port: state.port,
      connected_at: if(connected, do: state.connected_at),
      uptime_seconds: if(connected, do: DateTime.diff(DateTime.utc_now(), state.connected_at), else: 0),
      login_id: state.login_params.user_id,
      filter: state.login_params.filter,
      packet_stats: state.packet_stats,
      stored_packet_count: Aprsme.Packets.get_total_packet_count(),
      oldest_packet_timestamp: Aprsme.Packets.get_oldest_packet_timestamp()
    }

    {:reply, status, state}
  end

  @impl true
  def handle_info(:aprsme_no_message_timeout, state) do
    case state.socket do
      nil ->
        Logger.debug("Timeout occurred while not connected - ignoring")
        {:noreply, state}

      _socket ->
        Logger.error("Socket timeout detected. Killing genserver.")
        {:stop, :aprsme_timeout, state}
    end
  end

  @impl true
  def handle_info(:send_keepalive, state) do
    case state.socket do
      nil ->
        Logger.debug("Skipping keepalive - not connected to APRS-IS")
        # Keep rescheduling so the timer stays alive for when we reconnect
        keepalive_timer = create_keepalive_timer(@keepalive_interval)
        {:noreply, %{state | keepalive_timer: keepalive_timer}}

      socket ->
        # Send a comment line as keepalive (APRS-IS standard)
        case :gen_tcp.send(socket, "# keepalive\r\n") do
          :ok ->
            Logger.debug("Sent keepalive")
            keepalive_timer = create_keepalive_timer(@keepalive_interval)
            {:noreply, %{state | keepalive_timer: keepalive_timer}}

          {:error, reason} ->
            Logger.error("Failed to send keepalive: #{inspect(reason)}")
            {:stop, :normal, state}
        end
    end
  end

  @impl true
  def handle_info({:tcp, _socket, data}, state) do
    handle_socket_data(data, state)
  end

  @impl true
  def handle_info({:backpressure, :activate}, %{socket: nil} = state) do
    {:noreply, state}
  end

  def handle_info({:backpressure, :activate}, %{backpressure_active: true} = state) do
    {:noreply, state}
  end

  def handle_info({:backpressure, :activate}, state) do
    :inet.setopts(state.socket, active: false)
    cancel_timer(state.timer)
    safety_valve_timer = Process.send_after(self(), :backpressure_safety_valve, @backpressure_safety_valve_timeout)

    Logger.warning("Backpressure activated — socket set to passive mode")

    {:noreply, %{state | backpressure_active: true, timer: nil, safety_valve_timer: safety_valve_timer}}
  end

  @impl true
  def handle_info({:backpressure, :deactivate}, %{backpressure_active: false} = state) do
    {:noreply, state}
  end

  def handle_info({:backpressure, :deactivate}, %{socket: nil} = state) do
    cancel_safety_valve(state)
    {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil}}
  end

  def handle_info({:backpressure, :deactivate}, state) do
    :inet.setopts(state.socket, active: true)
    cancel_safety_valve(state)
    timer = create_timer(@aprs_timeout)

    Logger.info("Backpressure deactivated — socket resumed active mode")

    {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil, timer: timer}}
  end

  @impl true
  def handle_info(:backpressure_safety_valve, %{backpressure_active: false} = state) do
    {:noreply, state}
  end

  def handle_info(:backpressure_safety_valve, %{socket: nil} = state) do
    {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil}}
  end

  def handle_info(:backpressure_safety_valve, state) do
    Logger.warning("Backpressure safety valve triggered — forcing resume after timeout")
    :inet.setopts(state.socket, active: true)
    timer = create_timer(@aprs_timeout)

    {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil, timer: timer}}
  end

  @impl true
  def handle_info({:tcp_closed, _socket}, state) do
    Logger.warning("Socket has been closed by remote server - will reconnect")
    # Cancel any existing timers
    cancel_timer(state.timer)
    cancel_keepalive_timer(state.keepalive_timer)
    cancel_safety_valve(state)

    # Schedule reconnect
    schedule_reconnect(5000)

    state =
      track_failure(%{
        state
        | socket: nil,
          timer: nil,
          keepalive_timer: nil,
          backpressure_active: false,
          safety_valve_timer: nil
      })

    {:noreply, state}
  end

  @impl true
  def handle_info({:tcp_error, _socket, reason}, state) do
    Logger.error("Connection error: #{inspect(reason)} - will reconnect")
    # Cancel any existing timers
    cancel_timer(state.timer)
    cancel_keepalive_timer(state.keepalive_timer)
    cancel_safety_valve(state)

    # Schedule reconnect
    schedule_reconnect(5000)

    state =
      track_failure(%{
        state
        | socket: nil,
          timer: nil,
          keepalive_timer: nil,
          backpressure_active: false,
          safety_valve_timer: nil
      })

    {:noreply, state}
  end

  @impl true
  def handle_info(:reconnect, state) do
    Logger.info("Attempting to reconnect to APRS-IS...")

    case connect_to_aprs_is(state.server, state.port) do
      {:ok, socket} ->
        case send_login_string(
               socket,
               state.login_params.user_id,
               state.login_params.passcode,
               state.login_params.filter
             ) do
          :ok ->
            Logger.info("Successfully reconnected to APRS-IS")
            timer = create_timer(@aprs_timeout)
            keepalive_timer = create_keepalive_timer(@keepalive_interval)

            {:noreply,
             reset_failure(%{
               state
               | socket: socket,
                 timer: timer,
                 keepalive_timer: keepalive_timer,
                 backpressure_active: false,
                 safety_valve_timer: nil
             })}

          error ->
            Logger.warning("Failed to login to APRS-IS: #{inspect(error)}, will retry in 10 seconds")
            :gen_tcp.close(socket)
            schedule_reconnect(10_000)
            {:noreply, track_failure(state)}
        end

      error ->
        Logger.warning("Unable to reconnect to APRS-IS: #{inspect(error)}, will retry in 10 seconds")
        schedule_reconnect(10_000)
        {:noreply, track_failure(state)}
    end
  end

  defp handle_socket_data(data, state) do
    cancel_timer(state.timer)

    current_time = System.system_time(:second)
    packet_stats = update_packet_stats(state.packet_stats, current_time)

    buffer = state.buffer <> data
    {complete_lines, remaining_buffer} = extract_complete_lines(buffer)

    Enum.each(complete_lines, fn line ->
      trimmed = String.trim(line)
      if trimmed != "", do: dispatch(trimmed)
    end)

    timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout)

    {:noreply, %{state | timer: timer, packet_stats: packet_stats, buffer: remaining_buffer}}
  end

  # Extract complete lines from buffer, returning {complete_lines, remaining_buffer}
  @spec extract_complete_lines(String.t()) :: {[String.t()], String.t()}
  defp extract_complete_lines(buffer) do
    # Split by both \r\n and \n to handle different line endings
    parts = String.split(buffer, ~r/\r?\n/, parts: :infinity)

    # The last part might be incomplete
    case parts do
      [] ->
        {[], ""}

      [single] ->
        # No newline found, entire buffer is incomplete
        {[], single}

      parts ->
        # Last element might be incomplete line
        {complete, [maybe_incomplete]} = Enum.split(parts, -1)

        # If buffer ended with newline, maybe_incomplete will be empty string
        if String.ends_with?(buffer, "\n") or String.ends_with?(buffer, "\r\n") do
          {complete ++ [maybe_incomplete], ""}
        else
          {complete, maybe_incomplete}
        end
    end
  end

  @impl true
  def terminate(reason, state) do
    # Do Shutdown Stuff
    Logger.info("Terminating APRS-IS connection: #{inspect(reason)}")

    # Log any remaining buffered data
    case Map.get(state, :buffer, "") do
      "" -> :ok
      buffer -> Logger.warning("Terminating with incomplete packet in buffer: #{inspect(buffer)}")
    end

    # Cancel timers
    if timer = Map.get(state, :timer), do: Process.cancel_timer(timer)
    if timer = Map.get(state, :keepalive_timer), do: Process.cancel_timer(timer)
    if timer = Map.get(state, :safety_valve_timer), do: Process.cancel_timer(timer)

    # Close socket
    case Map.get(state, :socket) do
      nil ->
        :ok

      socket ->
        Logger.info("Closing socket")
        :gen_tcp.close(socket)
    end

    :normal
  end

  @impl true
  def code_change(_old_vsn, state, _extra) do
    {:ok, state}
  end

  @spec dispatch(binary) :: nil | :ok
  def dispatch("# logresp " <> rest) do
    if String.contains?(rest, "unverified") do
      Logger.warning("APRS-IS login unverified: #{String.trim(rest)}")
    else
      Logger.info("APRS-IS login accepted: #{String.trim(rest)}")
    end
  end

  def dispatch("#" <> comment_text) do
    Logger.debug("COMMENT: " <> String.trim(comment_text))
  end

  def dispatch(""), do: nil

  def dispatch(message) do
    case Aprs.parse(message) do
      {:ok, parsed_message} ->
        # Store the packet in the database for future replay
        # Use GenStage pipeline for efficient batch processing
        try do
          # Set received_at and convert struct to map — PacketConsumer.prepare_packet_for_insert
          # handles extract_additional_data and normalize_data_type, so don't duplicate here.
          current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)

          attrs =
            parsed_message
            |> Map.put(:received_at, current_time)
            |> Map.put(:raw, message)
            |> struct_to_map()

          Aprsme.PacketProducer.submit_packet(attrs)
        rescue
          error ->
            Logger.error("Exception while submitting packet from #{inspect(parsed_message.sender)}: #{inspect(error)}")
            Logger.debug("Raw message: #{inspect(message)}")
            Logger.debug("Parsed message: #{inspect(parsed_message)}")
        end

      {:error, :invalid_packet} ->
        Logger.debug("PARSE ERROR: invalid packet")

        Aprsme.Packets.store_bad_packet(message, %{
          message: "Invalid packet format",
          type: "ParseError"
        })

      {:error, error} ->
        Logger.debug("PARSE ERROR: " <> to_string(error))
        Aprsme.Packets.store_bad_packet(message, %{message: error, type: "ParseError"})
    end
  end

  @spec update_packet_stats(PacketStats.t(), integer()) :: PacketStats.t()
  defp update_packet_stats(stats, current_time) do
    new_total = stats.total_packets + 1

    # Check if we need to reset the per-second counter
    if current_time - stats.last_second_timestamp >= 1 do
      %PacketStats{
        total_packets: new_total,
        last_packet_at: DateTime.utc_now(),
        packets_per_second: 1,
        last_second_count: 1,
        last_second_timestamp: current_time
      }
    else
      new_second_count = stats.last_second_count + 1

      %{
        stats
        | total_packets: new_total,
          last_packet_at: DateTime.utc_now(),
          packets_per_second: new_second_count,
          last_second_count: new_second_count
      }
    end
  end

  @spec server_to_string(String.t() | charlist() | any()) :: String.t()
  defp server_to_string(server) when is_list(server), do: List.to_string(server)
  defp server_to_string(server) when is_binary(server), do: server
  defp server_to_string(server), do: to_string(server)

  @spec default_packet_stats() :: PacketStats.t()
  defp default_packet_stats do
    %PacketStats{last_second_timestamp: System.system_time(:second)}
  end

  # Helper function to recursively convert structs to maps
  # This handles nested structs that Map.from_struct/1 cannot handle
  @spec struct_to_map(any()) :: any()
  defp struct_to_map(%{__struct__: struct_type} = struct) do
    converted_map =
      struct
      |> Map.from_struct()
      |> Map.new(fn {k, v} -> {k, struct_to_map(v)} end)

    # Add type information to help with later processing
    Map.put(converted_map, :__original_struct__, struct_type)
  end

  defp struct_to_map(value) when is_list(value) do
    Enum.map(value, &struct_to_map/1)
  end

  defp struct_to_map(value), do: value

  defp cancel_safety_valve(%{safety_valve_timer: nil}), do: :ok

  defp cancel_safety_valve(%{safety_valve_timer: timer}) do
    Process.cancel_timer(timer)
    :ok
  end

  defp cancel_timer(nil), do: :ok
  defp cancel_timer(ref), do: Process.cancel_timer(ref)

  defp cancel_keepalive_timer(nil), do: :ok
  defp cancel_keepalive_timer(ref), do: Process.cancel_timer(ref)

  @spec schedule_reconnect(non_neg_integer()) :: reference()
  defp schedule_reconnect(delay) do
    Process.send_after(self(), :reconnect, delay)
  end

  # Track a connection failure. If we've been failing continuously for
  # @fallback_timeout_seconds, switch to the fallback server until next restart.
  defp track_failure(state) do
    now = DateTime.utc_now()
    failure_started_at = state.failure_started_at || now

    if DateTime.diff(now, failure_started_at) >= @fallback_timeout_seconds and
         not Application.get_env(:aprsme, :aprs_is_using_fallback, false) do
      Logger.warning(
        "Failed to connect to APRS-IS for #{@fallback_timeout_seconds}s, " <>
          "falling back to #{@fallback_server}"
      )

      Application.put_env(:aprsme, :aprs_is_using_fallback, true)
      %{state | failure_started_at: failure_started_at, server: @fallback_server}
    else
      %{state | failure_started_at: failure_started_at}
    end
  end

  # Reset the failure tracking on a successful connection.
  defp reset_failure(state) do
    %{state | failure_started_at: nil}
  end
end
