defmodule Raxol.Minimal do
  @moduledoc """
  Minimal Raxol terminal interface for ultra-fast startup.

  This module provides a stripped-down version of Raxol optimized for
  minimal startup time and memory footprint while still providing core
  terminal functionality.

  ## Features Included
  - Core terminal emulation with full ANSI/CSI support
  - Efficient buffer management with scrollback
  - Keyboard/mouse input handling
  - Performance telemetry
  - Character sets (UTF-8, ASCII)
  - Color support (16/256/RGB)

  ## Features Excluded
  - Web interface (Phoenix)
  - Database layer
  - Advanced animations
  - Plugin system (unless explicitly enabled)
  - Audit logging
  - Enterprise features

  ## Usage

      # Start minimal terminal
      {:ok, terminal} = Raxol.Minimal.start_terminal()

      # Or start with options
      {:ok, terminal} = Raxol.Minimal.start_terminal(
        width: 80,
        height: 24,
        mode: :raw,
        features: [:colors, :scrollback]
      )

  ## Configuration

  The minimal terminal can be configured via application environment:

      config :raxol, Raxol.Minimal,
        default_width: 80,
        default_height: 24,
        max_scrollback: 1000,
        enable_telemetry: true
  """

  use Raxol.Core.Behaviours.BaseManager
  alias Raxol.Terminal.ANSI.Utils.AnsiParser
  alias Raxol.Terminal.Buffer
  alias Raxol.Terminal.Buffer.Cell
  alias Raxol.Terminal.ScreenBuffer.Operations
  alias Raxol.Core.Utils.GenServerHelpers
  alias Raxol.Core.Runtime.Log

  @type terminal_mode :: :raw | :cooked | :cbreak
  @type color_mode :: :none | :ansi16 | :ansi256 | :rgb
  @type feature ::
          :colors | :scrollback | :mouse | :bracketed_paste | :alternate_screen

  @type terminal_options :: [
          width: pos_integer(),
          height: pos_integer(),
          mode: terminal_mode(),
          color_mode: color_mode(),
          features: [feature()],
          scrollback_lines: non_neg_integer(),
          charset: :utf8 | :ascii,
          telemetry: boolean(),
          hibernate_after: timeout()
        ]

  defmodule State do
    @moduledoc false

    defstruct [
      :width,
      :height,
      :mode,
      :color_mode,
      :features,
      :main_buffer,
      :alternate_buffer,
      :active_buffer,
      :scrollback,
      :cursor,
      :saved_cursor,
      :char_attrs,
      :charset,
      :started_at,
      :telemetry_enabled,
      :metrics,
      :input_buffer,
      :parser_state
    ]

    @type t :: %__MODULE__{
            width: pos_integer(),
            height: pos_integer(),
            mode: Raxol.Minimal.terminal_mode(),
            color_mode: Raxol.Minimal.color_mode(),
            features: MapSet.t(Raxol.Minimal.feature()),
            main_buffer: Buffer.t(),
            alternate_buffer: Buffer.t() | nil,
            active_buffer: :main | :alternate,
            scrollback: list(list(Cell.t())),
            cursor: {non_neg_integer(), non_neg_integer()},
            saved_cursor: {non_neg_integer(), non_neg_integer()} | nil,
            char_attrs: map(),
            charset: :utf8 | :ascii,
            started_at: integer(),
            telemetry_enabled: boolean(),
            metrics: map(),
            input_buffer: binary(),
            parser_state: term()
          }
  end

  @default_options [
    width: 80,
    height: 24,
    mode: :raw,
    color_mode: :ansi256,
    features: [:colors, :scrollback],
    scrollback_lines: 1000,
    charset: :utf8,
    telemetry: true,
    hibernate_after: 15_000
  ]

  # Public API

  @doc """
  Start a minimal terminal session with ultra-fast startup.

  Returns `{:ok, pid}` on success.
  """
  @spec start_terminal(terminal_options()) :: {:ok, pid()} | {:error, term()}
  def start_terminal(opts \\ []) do
    merged_opts = Keyword.merge(get_config_defaults(), opts)

    # Use BaseManager's start_link
    start_link =
      Application.get_env(:raxol, :start_link_fn, &GenServer.start_link/3)

    start_link.(__MODULE__, merged_opts,
      hibernate_after: merged_opts[:hibernate_after]
    )
  end

  @doc """
  Get current terminal state for debugging.
  """
  @spec get_state(pid()) :: map()
  def get_state(pid) do
    GenServer.call(pid, :get_state)
  end

  @doc """
  Send input to terminal.
  """
  @spec send_input(pid(), binary()) :: :ok
  def send_input(pid, data) do
    GenServer.cast(pid, {:input, data})
  end

  @doc """
  Get terminal dimensions.
  """
  @spec get_dimensions(pid()) :: {pos_integer(), pos_integer()}
  def get_dimensions(pid) do
    GenServer.call(pid, :get_dimensions)
  end

  @doc """
  Resize the terminal.
  """
  @spec resize(pid(), pos_integer(), pos_integer()) :: :ok
  def resize(pid, width, height) do
    GenServer.call(pid, {:resize, width, height})
  end

  @doc """
  Get performance metrics.
  """
  @spec get_metrics(pid()) :: map()
  def get_metrics(pid) do
    GenServer.call(pid, :get_metrics)
  end

  @doc """
  Clear the terminal screen.
  """
  @spec clear(pid()) :: :ok
  def clear(pid) do
    GenServer.cast(pid, :clear)
  end

  @doc """
  Reset terminal to initial state.
  """
  @spec reset(pid()) :: :ok
  def reset(pid) do
    GenServer.cast(pid, :reset)
  end

  # BaseManager Implementation

  @impl Raxol.Core.Behaviours.BaseManager
  def init_manager(opts) do
    start_time = System.monotonic_time(:microsecond)

    state = initialize_state(opts)

    startup_time = System.monotonic_time(:microsecond) - start_time

    with true <- state.telemetry_enabled do
      :telemetry.execute(
        [:raxol, :minimal, :startup],
        %{duration: startup_time},
        %{width: state.width, height: state.height}
      )
    end

    Log.info("Minimal terminal started in #{startup_time}μs")

    {:ok, state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_call(:get_state, _from, state) do
    GenServerHelpers.handle_get_state(state)
  end

  def handle_manager_call(:get_dimensions, _from, state) do
    {:reply, {state.width, state.height}, state}
  end

  def handle_manager_call({:resize, width, height}, _from, state) do
    new_state = resize_terminal(state, width, height)
    {:reply, :ok, new_state}
  end

  def handle_manager_call(:get_metrics, _from, state) do
    GenServerHelpers.handle_get_metrics(state)
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_cast({:input, data}, state) do
    start_time =
      case state.telemetry_enabled do
        true -> System.monotonic_time(:microsecond)
        false -> nil
      end

    new_state = process_input(data, state)

    final_state =
      case {state.telemetry_enabled, start_time} do
        {true, start_time} when is_integer(start_time) ->
          duration = System.monotonic_time(:microsecond) - start_time

          :telemetry.execute(
            [:raxol, :minimal, :input],
            %{duration: duration, bytes: byte_size(data)},
            %{}
          )

          update_metrics(new_state, :input_processing, duration)

        {false, _} ->
          new_state
      end

    {:noreply, final_state}
  end

  def handle_manager_cast(:clear, state) do
    new_buffer = Buffer.clear(get_active_buffer(state))
    new_state = put_active_buffer(state, new_buffer)
    {:noreply, %{new_state | cursor: {0, 0}}}
  end

  def handle_manager_cast(:reset, state) do
    {:noreply, initialize_state(get_init_opts(state))}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_info(:hibernate, state) do
    {:noreply, state, :hibernate}
  end

  def handle_manager_info(_msg, state) do
    {:noreply, state}
  end

  # Private Functions

  defp get_config_defaults do
    Application.get_env(:raxol, __MODULE__, [])
    |> Keyword.merge(@default_options)
  end

  defp initialize_state(opts) do
    width = opts[:width]
    height = opts[:height]
    features = MapSet.new(opts[:features] || [])

    main_buffer = Buffer.new({width, height})

    alternate_buffer =
      case MapSet.member?(features, :alternate_screen) do
        true -> Buffer.new({width, height})
        false -> nil
      end

    %State{
      width: width,
      height: height,
      mode: opts[:mode],
      color_mode: opts[:color_mode],
      features: features,
      main_buffer: main_buffer,
      alternate_buffer: alternate_buffer,
      active_buffer: :main,
      scrollback: [],
      cursor: {0, 0},
      saved_cursor: nil,
      char_attrs: default_char_attrs(),
      charset: opts[:charset],
      started_at: System.monotonic_time(:millisecond),
      telemetry_enabled: opts[:telemetry],
      metrics: %{
        input_count: 0,
        output_count: 0,
        avg_input_time: 0,
        max_input_time: 0,
        memory_bytes: 0
      },
      input_buffer: <<>>,
      parser_state: nil
    }
  end

  defp default_char_attrs do
    %{
      bold: false,
      italic: false,
      underline: false,
      blink: false,
      reverse: false,
      hidden: false,
      strikethrough: false,
      fg_color: :default,
      bg_color: :default
    }
  end

  defp process_input(data, state) do
    # Parse ANSI sequences using the full parser
    sequences = AnsiParser.parse(data)

    Enum.reduce(sequences, state, fn sequence, acc_state ->
      handle_sequence(sequence, acc_state)
    end)
  end

  defp handle_sequence(%{type: :csi} = seq, state) do
    handle_csi_sequence(seq, state)
  end

  defp handle_sequence(%{type: :osc} = seq, state) do
    handle_osc_sequence(seq, state)
  end

  defp handle_sequence(%{type: :text, text: text}, state) do
    write_text(state, text)
  end

  defp handle_sequence(_, state) do
    # Ignore unsupported sequences in minimal mode
    state
  end

  defp handle_csi_sequence(%{command: command, params: params}, state) do
    case command do
      # Cursor movement
      "A" -> move_cursor(state, :up, parse_param(params, 0, 1))
      "B" -> move_cursor(state, :down, parse_param(params, 0, 1))
      "C" -> move_cursor(state, :right, parse_param(params, 0, 1))
      "D" -> move_cursor(state, :left, parse_param(params, 0, 1))
      "H" -> set_cursor_position(state, params)
      "f" -> set_cursor_position(state, params)
      # Erasing
      "J" -> erase_display(state, parse_param(params, 0, 0))
      "K" -> erase_line(state, parse_param(params, 0, 0))
      # Scrolling
      "S" -> scroll_up(state, parse_param(params, 0, 1))
      "T" -> scroll_down(state, parse_param(params, 0, 1))
      # Graphics
      "m" -> set_graphics_rendition(state, params)
      # Save/Restore cursor
      "s" -> %{state | saved_cursor: state.cursor}
      "u" -> %{state | cursor: state.saved_cursor || state.cursor}
      _ -> state
    end
  end

  defp handle_osc_sequence(%{params: params}, state) do
    case params do
      ["0", title] -> set_window_title(state, title)
      ["2", title] -> set_window_title(state, title)
      _ -> state
    end
  end

  defp parse_param(params, index, default) do
    case Enum.at(params, index) do
      nil -> default
      "" -> default
      param -> String.to_integer(param)
    end
  end

  defp move_cursor(state, direction, count) do
    {x, y} = state.cursor

    new_cursor =
      case direction do
        :up -> {x, max(0, y - count)}
        :down -> {x, min(state.height - 1, y + count)}
        :left -> {max(0, x - count), y}
        :right -> {min(state.width - 1, x + count), y}
      end

    %{state | cursor: new_cursor}
  end

  defp set_cursor_position(state, params) do
    row = parse_param(params, 0, 1) - 1
    col = parse_param(params, 1, 1) - 1

    new_cursor = {
      max(0, min(state.width - 1, col)),
      max(0, min(state.height - 1, row))
    }

    %{state | cursor: new_cursor}
  end

  defp write_text(state, text) do
    buffer = get_active_buffer(state)
    {x, y} = state.cursor

    {new_buffer, new_x, new_y} =
      text
      |> String.graphemes()
      |> Enum.reduce({buffer, x, y}, fn char, {buf, cur_x, cur_y} ->
        case char do
          "\r" ->
            {buf, 0, cur_y}

          "\n" ->
            new_y = min(state.height - 1, cur_y + 1)
            {buf, 0, new_y}

          "\t" ->
            # Move to next tab stop (every 8 columns)
            new_x = min(state.width - 1, (div(cur_x, 8) + 1) * 8)
            {buf, new_x, cur_y}

          _ ->
            cond do
              cur_x < state.width and cur_y < state.height ->
                # Write character directly to buffer
                cell = Cell.new(char, state.char_attrs)
                new_buf = Buffer.set_cell(buf, cur_x, cur_y, cell)
                {new_buf, cur_x + 1, cur_y}

              cur_x >= state.width and cur_y < state.height - 1 ->
                # Wrap to next line if at end of line
                cell = Cell.new(char, state.char_attrs)
                new_buf = Buffer.set_cell(buf, 0, cur_y + 1, cell)
                {new_buf, 1, cur_y + 1}

              true ->
                {buf, cur_x, cur_y}
            end
        end
      end)

    new_state = put_active_buffer(state, new_buffer)
    %{new_state | cursor: {new_x, new_y}}
  end

  defp erase_display(state, mode) do
    buffer = get_active_buffer(state)
    {x, y} = state.cursor

    new_buffer =
      case mode do
        0 -> Operations.Erasing.erase_in_display(buffer, 0, {y, x})
        1 -> Operations.Erasing.erase_in_display(buffer, 1, {y, x})
        2 -> Buffer.clear(buffer)
        # Clear with scrollback not directly available
        3 -> Buffer.clear(buffer)
        _ -> buffer
      end

    put_active_buffer(state, new_buffer)
  end

  defp erase_line(state, mode) do
    buffer = get_active_buffer(state)
    {x, y} = state.cursor

    new_buffer =
      case mode do
        0 -> Operations.Erasing.erase_in_line(buffer, 0, {y, x})
        1 -> Operations.Erasing.erase_in_line(buffer, 1, {y, x})
        2 -> Operations.Erasing.erase_in_line(buffer, 2, {y, x})
        _ -> buffer
      end

    put_active_buffer(state, new_buffer)
  end

  defp scroll_up(state, lines) do
    buffer = get_active_buffer(state)
    new_buffer = Operations.scroll_up(buffer, lines)
    put_active_buffer(state, new_buffer)
  end

  defp scroll_down(state, lines) do
    buffer = get_active_buffer(state)
    new_buffer = Operations.scroll_down(buffer, lines)
    put_active_buffer(state, new_buffer)
  end

  defp set_graphics_rendition(state, params) do
    attrs =
      params
      |> Enum.map(&parse_sgr_param/1)
      |> Enum.reduce(state.char_attrs, &apply_sgr/2)

    %{state | char_attrs: attrs}
  end

  defp parse_sgr_param(""), do: 0
  defp parse_sgr_param(param), do: String.to_integer(param)

  defp apply_sgr(0, _attrs), do: default_char_attrs()
  defp apply_sgr(1, attrs), do: %{attrs | bold: true}
  defp apply_sgr(3, attrs), do: %{attrs | italic: true}
  defp apply_sgr(4, attrs), do: %{attrs | underline: true}
  defp apply_sgr(5, attrs), do: %{attrs | blink: true}
  defp apply_sgr(7, attrs), do: %{attrs | reverse: true}
  defp apply_sgr(8, attrs), do: %{attrs | hidden: true}
  defp apply_sgr(9, attrs), do: %{attrs | strikethrough: true}

  defp apply_sgr(n, attrs) when n >= 30 and n <= 37,
    do: %{attrs | fg_color: ansi_color(n - 30)}

  defp apply_sgr(n, attrs) when n >= 40 and n <= 47,
    do: %{attrs | bg_color: ansi_color(n - 40)}

  defp apply_sgr(n, attrs) when n >= 90 and n <= 97,
    do: %{attrs | fg_color: bright_ansi_color(n - 90)}

  defp apply_sgr(n, attrs) when n >= 100 and n <= 107,
    do: %{attrs | bg_color: bright_ansi_color(n - 100)}

  defp apply_sgr(_, attrs), do: attrs

  defp ansi_color(0), do: :black
  defp ansi_color(1), do: :red
  defp ansi_color(2), do: :green
  defp ansi_color(3), do: :yellow
  defp ansi_color(4), do: :blue
  defp ansi_color(5), do: :magenta
  defp ansi_color(6), do: :cyan
  defp ansi_color(7), do: :white
  defp ansi_color(_), do: :default

  defp bright_ansi_color(n), do: {:bright, ansi_color(n)}

  defp set_window_title(state, _title) do
    # In minimal mode, we don't actually set window titles
    # but we track the attempt for compatibility
    state
  end

  defp resize_terminal(state, width, height) do
    main_buffer = Buffer.resize(state.main_buffer, width, height)

    alternate_buffer =
      case state.alternate_buffer do
        nil -> nil
        buffer -> Buffer.resize(buffer, width, height)
      end

    {cur_x, cur_y} = state.cursor

    new_cursor = {
      min(cur_x, width - 1),
      min(cur_y, height - 1)
    }

    %{
      state
      | width: width,
        height: height,
        main_buffer: main_buffer,
        alternate_buffer: alternate_buffer,
        cursor: new_cursor
    }
  end

  defp get_active_buffer(%State{
         active_buffer: :alternate,
         alternate_buffer: buffer
       })
       when not is_nil(buffer),
       do: buffer

  defp get_active_buffer(%State{main_buffer: buffer}), do: buffer

  defp put_active_buffer(%State{active_buffer: :alternate} = state, buffer)
       when not is_nil(state.alternate_buffer) do
    %{state | alternate_buffer: buffer}
  end

  defp put_active_buffer(state, buffer) do
    %{state | main_buffer: buffer}
  end

  defp update_metrics(state, :input_processing, duration) do
    metrics = state.metrics
    count = metrics.input_count + 1
    avg = (metrics.avg_input_time * metrics.input_count + duration) / count
    max_time = max(metrics.max_input_time, duration)

    memory =
      case rem(count, 100) do
        0 ->
          {:memory, bytes} = Process.info(self(), :memory)
          bytes

        _ ->
          metrics.memory_bytes
      end

    new_metrics = %{
      metrics
      | input_count: count,
        avg_input_time: avg,
        max_input_time: max_time,
        memory_bytes: memory
    }

    %{state | metrics: new_metrics}
  end

  defp get_init_opts(state) do
    [
      width: state.width,
      height: state.height,
      mode: state.mode,
      color_mode: state.color_mode,
      features: MapSet.to_list(state.features),
      charset: state.charset,
      telemetry: state.telemetry_enabled
    ]
  end
end
