defmodule Raxol.Terminal.Driver do
  @moduledoc """
  Handles raw terminal input/output and event generation.

  Responsibilities:
  - Setting terminal mode (raw, echo)
  - Reading input events via termbox2_nif NIF
  - Parsing input events into `Raxol.Core.Events.Event` structs
  - Detecting terminal resize events
  - Sending parsed events to the `Dispatcher`
  - Restoring terminal state on exit
  """

  alias Raxol.Core.Runtime.Log
  use Raxol.Core.Behaviours.BaseManager

  require Logger
  require Raxol.Core.Runtime.Log
  # Import Bitwise for bitwise operations
  # import Bitwise

  alias Raxol.Core.Events.Event
  alias Raxol.Terminal.ANSI.InputParser
  alias Raxol.Terminal.Driver.Dispatch
  alias Raxol.Terminal.Driver.EventTranslator
  alias Raxol.Terminal.Driver.InputBuffer
  alias Raxol.Terminal.Driver.TermboxLifecycle

  @compile {:no_warn_undefined, Raxol.Terminal.Driver.Dispatch}
  @compile {:no_warn_undefined, Raxol.Terminal.Driver.EventTranslator}
  @compile {:no_warn_undefined, Raxol.Terminal.Driver.InputBuffer}
  @compile {:no_warn_undefined, Raxol.Terminal.Driver.TermboxLifecycle}

  @input_buffer_flush_ms 50

  # Check if termbox2_nif is available at compile time
  @termbox2_available Code.ensure_loaded?(:termbox2_nif)

  import Raxol.Terminal.TerminalUtils, only: [has_terminal_device?: 0]

  alias Raxol.Terminal.Env

  # Constants for retry logic
  @max_init_retries 3
  # ms
  @init_retry_delay 1000

  # Allow nil initially
  @type dispatcher_pid :: pid() | nil
  @type original_stty :: String.t()
  @type termbox_state :: :uninitialized | :initialized | :failed

  defmodule State do
    @moduledoc false
    defstruct dispatcher_pid: nil,
              original_stty: nil,
              termbox_state: :uninitialized,
              init_retries: 0,
              io_terminal_state: nil,
              input_buffer: <<>>,
              flush_timer: nil
  end

  # --- Public API ---

  @doc """
  Returns the current terminal backend being used.

  ## Examples

      iex> Raxol.Terminal.Driver.backend()
      :termbox2_nif

      iex> Raxol.Terminal.Driver.backend()
      :io_terminal
  """
  # The spec covers both possible return values across platforms.
  # On any given compilation, only one branch is reachable due to
  # @termbox2_available being a compile-time constant.
  @dialyzer {:nowarn_function, backend: 0}
  @spec backend() :: :termbox2_nif | :io_terminal
  def backend do
    if @termbox2_available, do: :termbox2_nif, else: :io_terminal
  end

  # BaseManager provides start_link/1 and start_link/2 automatically
  # We can override if needed but the dispatcher_pid is passed as init argument

  # --- BaseManager Callbacks ---

  # Logger.configure(level: :none) works at runtime but :none isn't in Logger's typespec
  @dialyzer {:nowarn_function, init_manager: 1}
  @impl true
  def init_manager(opts) do
    # Extract dispatcher_pid from opts - handle both keyword list and raw value
    dispatcher_pid = extract_dispatcher_pid(opts)
    mouse_enabled = if is_list(opts), do: Keyword.get(opts, :mouse, true), else: true

    Raxol.Core.Runtime.Log.info(
      "[#{__MODULE__}] init called with dispatcher: #{inspect(dispatcher_pid)}"
    )

    # Get original terminal settings using Erlang IO (no subprocess needed)
    output =
      case {:io.rows(), :io.columns()} do
        {{:ok, rows}, {:ok, cols}} -> "#{rows} #{cols}"
        _ -> "80 24"
      end

    state = %State{
      dispatcher_pid: dispatcher_pid,
      original_stty: output,
      termbox_state: :uninitialized,
      init_retries: 0
    }

    # Initialize terminal in raw mode only if attached to a TTY.
    # Use has_terminal_device?() instead of real_tty?() because the latter
    # relies on :io.columns() which fails in -noshell mode (mix run).
    tty_detected = has_terminal_device?()

    case {Env.test?(), tty_detected, dispatcher_pid} do
      {true, _, nil} ->
        Raxol.Core.Runtime.Log.info(
          "[Driver] Test environment detected, sending driver_ready event"
        )

        Raxol.Core.Runtime.Log.warning_with_context(
          "[Driver] No dispatcher_pid provided, skipping driver_ready and initial resize event",
          %{}
        )

        state = %{state | termbox_state: :initialized}
        {:ok, state}

      {true, _, pid} ->
        Raxol.Core.Runtime.Log.info(
          "[Driver] Test environment detected, sending driver_ready event"
        )

        send(pid, {:driver_ready, self()})

        Raxol.Core.Runtime.Log.info(
          "[Driver] Sending initial resize event to dispatcher_pid: #{inspect(pid)}"
        )

        Dispatch.send_initial_resize_event(pid)
        state = %{state | termbox_state: :initialized}
        {:ok, state}

      {_, _, nil} ->
        # No dispatcher — this is the Application supervisor's placeholder Driver.
        # Don't set up the terminal; the Lifecycle's Driver will do that.
        Raxol.Core.Runtime.Log.info("[TerminalDriver] No dispatcher, skipping terminal setup.")

        {:ok, state}

      {_, true, _} ->
        Raxol.Core.Runtime.Log.info(
          "[TerminalDriver] TTY detected, initializing ANSI terminal..."
        )

        # Save original TTY settings via /dev/tty (System.cmd pipes stdin,
        # so we must redirect from /dev/tty for stty to affect the real terminal)
        original_stty = Raxol.Terminal.Driver.Stty.save()

        # Raw mode on the actual terminal: no echo, no line buffering, no signals
        Raxol.Terminal.Driver.Stty.raw!()

        # Suppress Logger console output so it doesn't corrupt the TUI
        Logger.configure(level: :none)

        # Enter alternate screen, hide cursor
        IO.write("\e[?1049h\e[?25l")

        # Reset mouse tracking (may be left over from a crashed session)
        IO.write("\e[?1003l\e[?1006l\e[?1000l")

        # Enable SGR mouse mode (button events + SGR extended coordinates)
        if mouse_enabled do
          IO.write("\e[?1000h\e[?1006h")
        end

        # Enable terminal modes: focus reporting, bracketed paste
        IO.write("\e[?1004h\e[?2004h")

        # Send initial resize event if we have a dispatcher
        if dispatcher_pid,
          do: Dispatch.send_initial_resize_event(dispatcher_pid)

        # Activate prim_tty reader for input. In -noshell mode, prim_tty
        # was initialized with tty => false, so the reader gets no select
        # notifications. start_stdin_reader triggers reinit with tty => true
        # and sets up trace interception of the reader's output.
        start_stdin_reader(self())

        state = %{
          state
          | termbox_state: :initialized,
            original_stty: original_stty,
            io_terminal_state: %{
              input_reader: Process.whereis(:user_drv_reader),
              tty_fd: nil,
              tty_port: nil
            }
        }

        {:ok, state}

      {_, false, _} ->
        Raxol.Core.Runtime.Log.warning_with_context(
          "Not attached to a TTY. Skipping Termbox2Nif.tb_init(). Terminal features will be disabled.",
          %{}
        )

        {:ok, state}
    end
  end

  # --- BaseManager handle_info callbacks ---

  @impl true
  def handle_manager_info(:retry_init, %{init_retries: retries} = state)
      when retries < @max_init_retries do
    case TermboxLifecycle.initialize() do
      :ok ->
        Raxol.Core.Runtime.Log.info("Successfully initialized termbox on retry")
        {:noreply, %{state | termbox_state: :initialized}}

      {:error, reason} ->
        Raxol.Core.Runtime.Log.error(
          "Failed to initialize termbox on retry #{retries + 1}: #{inspect(reason)}"
        )

        Process.send_after(self(), :retry_init, @init_retry_delay)
        {:noreply, %{state | init_retries: retries + 1}}
    end
  end

  @impl true
  def handle_manager_info(:retry_init, state) do
    Raxol.Core.Runtime.Log.error(
      "Failed to initialize termbox after #{@max_init_retries} attempts. Terminal features will be disabled."
    )

    {:noreply, state}
  end

  @impl true
  def handle_manager_info(
        {:termbox_event, event_map},
        %{termbox_state: :initialized, dispatcher_pid: dispatcher_pid} = state
      ) do
    Raxol.Core.Runtime.Log.debug("Received termbox event: #{inspect(event_map)}")

    case EventTranslator.translate(event_map) do
      {:ok, %Event{} = event} ->
        # Only send if dispatcher_pid is known
        case dispatcher_pid do
          nil -> :ok
          pid -> Dispatch.send_event_to_dispatcher(pid, event)
        end

        {:noreply, state}

      :ignore ->
        # Event type we don't care about
        Raxol.Core.Runtime.Log.debug("[Driver] Ignoring termbox event: #{inspect(event_map)}")

        {:noreply, state}

      {:error, reason} ->
        Raxol.Core.Runtime.Log.warning_with_context(
          "Failed to translate termbox event: #{inspect(reason)}. Event: #{inspect(event_map)}",
          %{}
        )

        {:noreply, state}
    end
  end

  @impl true
  def handle_manager_info({:termbox_event, _event_map}, state) do
    # Ignore events if termbox is not initialized
    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:termbox_error, reason}, state) do
    Raxol.Core.Runtime.Log.error(
      "Received termbox error: #{inspect(reason)}. Attempting recovery..."
    )

    case state.termbox_state do
      :initialized -> TermboxLifecycle.handle_recovery(reason, state)
      _ -> {:stop, {:termbox_error, reason}, state}
    end
  end

  @impl true
  def handle_manager_info({:register_dispatcher, pid}, state)
      when is_pid(pid) do
    Raxol.Core.Runtime.Log.info("Registering dispatcher PID: #{inspect(pid)}")
    # Send initial size event now that we have the PID
    Dispatch.send_initial_resize_event(pid)
    {:noreply, %{state | dispatcher_pid: pid}}
  end

  @impl true
  def handle_manager_info(
        {:test_input, input_data},
        %{dispatcher_pid: nil} = state
      ) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "Received test input before dispatcher registration: #{inspect(input_data)}",
      %{}
    )

    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:test_input, input_data}, state) do
    # Construct a basic event. Tests might need more specific event types later.
    # We need to parse the input_data into something the MockApp expects.
    Raxol.Core.Runtime.Log.debug(
      "[TerminalDriver.handle_cast - :test_input] Received input_data: #{inspect(input_data)}, state: #{inspect(state)}"
    )

    event = Dispatch.parse_test_input(input_data)

    Raxol.Core.Runtime.Log.debug(
      "[TerminalDriver.handle_cast - :test_input] Parsed event: #{inspect(event)}"
    )

    Raxol.Core.Runtime.Log.debug("[TEST] Dispatching simulated event: #{inspect(event)}")

    GenServer.cast(state.dispatcher_pid, {:dispatch, event})
    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:EXIT, _pid, _reason}, state) do
    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:raw_input, data}, state) when is_binary(data) do
    buffer_and_dispatch(data, state)
  end

  # Trace messages from prim_tty reader — intercept input data
  @impl true
  def handle_manager_info(
        {:trace, _reader, :send, {_ref, {:data, data}}, _to},
        state
      ) do
    binary =
      cond do
        is_binary(data) -> data
        is_list(data) -> IO.iodata_to_binary(data)
        true -> <<>>
      end

    if byte_size(binary) > 0 do
      buffer_and_dispatch(binary, state)
    else
      {:noreply, state}
    end
  end

  # Ignore other trace messages from the reader (signals, receives, etc.)
  @impl true
  def handle_manager_info({:trace, _pid, :send, _msg, _to}, state) do
    {:noreply, state}
  end

  # Port data — accumulate and parse (buffering handles split escape sequences)
  @impl true
  def handle_manager_info({port, {:data, data}}, state) when is_port(port) do
    buffer_and_dispatch(data, state)
  end

  # Flush timer fired — dispatch whatever we have
  @impl true
  def handle_manager_info(:flush_input_buffer, state) do
    flush_buffer(%{state | flush_timer: nil})
  end

  # Port closed
  @impl true
  def handle_manager_info({port, :eof}, state) when is_port(port) do
    {:noreply, state}
  end

  @impl true
  def handle_manager_info({port, {:exit_status, _status}}, state)
      when is_port(port) do
    {:noreply, state}
  end

  @impl true
  def handle_manager_info(unhandled_message, state) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "#{__MODULE__} received unhandled message: #{inspect(unhandled_message)}",
      %{}
    )

    {:noreply, state}
  end

  defp buffer_and_dispatch(data, state) do
    buffer = state.input_buffer <> data
    _ = if state.flush_timer, do: Process.cancel_timer(state.flush_timer)

    if InputBuffer.incomplete_escape?(buffer) do
      timer = Process.send_after(self(), :flush_input_buffer, @input_buffer_flush_ms)
      {:noreply, %{state | input_buffer: buffer, flush_timer: timer}}
    else
      flush_buffer(%{state | input_buffer: buffer, flush_timer: nil})
    end
  end

  defp dispatch_raw_input(data, state) do
    events = InputParser.parse(data)

    Enum.each(events, fn event ->
      case state.dispatcher_pid do
        nil -> :ok
        pid -> Dispatch.send_event_to_dispatcher(pid, event)
      end
    end)

    {:noreply, state}
  end

  # Forward cast messages to handle_info for test_input
  @impl true
  def handle_manager_cast({:test_input, input_data}, state) do
    handle_manager_info({:test_input, input_data}, state)
  end

  # Private helper to extract dispatcher_pid from init opts
  defp extract_dispatcher_pid(opts) when is_list(opts) do
    Keyword.get(opts, :dispatcher_pid)
  end

  defp extract_dispatcher_pid(pid) when is_pid(pid), do: pid
  defp extract_dispatcher_pid(_), do: nil

  def terminate(_reason, %{termbox_state: :initialized} = state) do
    Raxol.Core.Runtime.Log.info("Terminal Driver terminating.")
    TermboxLifecycle.cleanup_terminal(state)
  end

  def terminate(_reason, _state) do
    Raxol.Core.Runtime.Log.info("Terminal Driver terminating (not initialized).")

    :ok
  end

  @doc """
  Processes a terminal title change event.
  """
  def process_title_change(title, state) when is_binary(title) do
    _ =
      if not Env.test?() and has_terminal_device?() do
        if @termbox2_available do
          :termbox2_nif.tb_set_title(title)
        end
      end

    {:noreply, state}
  end

  @doc """
  Processes a terminal position change event.
  """
  def process_position_change(x, y, state)
      when is_integer(x) and is_integer(y) do
    _ =
      if not Env.test?() and has_terminal_device?() do
        if @termbox2_available do
          :termbox2_nif.tb_set_position(x, y)
        else
          0
        end
      end

    {:noreply, state}
  end

  # --- Input reader ---
  # In -noshell mode (mix run), prim_tty is initialized with tty => false,
  # so its reader process never receives select notifications. We trigger
  # reinit via user_drv:start_shell, then trace-intercept the reader's
  # data messages.
  defp start_stdin_reader(_driver_pid) do
    # In -noshell mode, user_drv initializes prim_tty with tty => false,
    # so the NIF never sets up the terminal fd for select notifications.
    # The reader process exists but is blocked waiting for events that
    # never arrive.
    #
    # Fix: call user_drv:start_shell to trigger prim_tty:reinit with
    # tty => true, which activates the terminal fd. Then trace the
    # reader to intercept input data before it reaches user_drv.
    reader = Process.whereis(:user_drv_reader)
    user_drv = Process.whereis(:user_drv)

    if user_drv do
      # Activate the terminal fd by triggering prim_tty reinit.
      try do
        :gen_statem.call(
          user_drv,
          {:start_shell, %{initial_shell: {__MODULE__, :noop_shell, []}}}
        )
      catch
        _, _ -> :ok
      end
    end

    if reader do
      # Trace the reader's sends to intercept data before user_drv
      # forwards it. The reader sends {ref, {:data, bytes}} to user_drv.
      :erlang.trace(reader, true, [:send])
    end

    # Return nil — no spawned reader pid to track. Input arrives via
    # trace messages in handle_manager_info.
    nil
  end

  @doc false
  def noop_shell, do: :ok

  # --- Input buffering ---
  # Escape sequences may span multiple messages, so we buffer until complete.

  defp flush_buffer(%{input_buffer: <<>>} = state), do: {:noreply, state}

  defp flush_buffer(state) do
    dispatch_raw_input(state.input_buffer, %{state | input_buffer: <<>>})
  end
end
