defmodule Raxol.Core.Runtime.Events.Dispatcher do
  @moduledoc """
  Manages the application state (model) and dispatches events to the application's
  `update/2` function. It also handles commands returned by `update/2`.
  """

  use Raxol.Core.Behaviours.BaseManager

  require Logger
  require Raxol.Core.Runtime.Log
  require Raxol.Core.Events.Event
  require Raxol.Core.Runtime.Command
  require Raxol.Core.UserPreferences

  alias Raxol.Core.Events.Event
  alias Raxol.Core.FocusManager
  alias Raxol.Core.Runtime.Application
  alias Raxol.Core.Runtime.Command
  alias Raxol.Core.Runtime.Events.Bubbler
  alias Raxol.Core.Runtime.Events.DispatcherHooks
  alias Raxol.Core.UserPreferences

  @registry_name :raxol_event_subscriptions

  defmodule State do
    @moduledoc false
    defstruct runtime_pid: nil,
              app_module: nil,
              model: nil,
              width: 0,
              height: 0,
              focused: true,
              debug_mode: false,
              plugin_manager: nil,
              plugin_manager_struct: nil,
              command_registry_table: nil,
              current_theme_id: :default,
              command_module: Raxol.Core.Runtime.Command,
              view_tree: nil,
              layout: [],
              rendering_engine: nil,
              time_travel: nil,
              cycle_profiler: nil
  end

  # BaseManager provides start_link/1 and start_link/2 automatically
  # Custom start_link to handle the runtime_pid and initial_state parameters
  def start_link(runtime_pid, initial_state, opts \\ []) do
    command_module =
      Keyword.get(opts, :command_module, Raxol.Core.Runtime.Command)

    server_opts =
      case Keyword.get(opts, :name, __MODULE__) do
        nil -> []
        name -> [name: name]
      end

    GenServer.start_link(
      __MODULE__,
      {runtime_pid, initial_state, command_module},
      server_opts
    )
  end

  @impl true
  def init_manager({runtime_pid, initial_state, command_module}) do
    state = %State{
      runtime_pid: runtime_pid,
      app_module: initial_state.app_module,
      model: initial_state.model,
      width: initial_state.width,
      height: initial_state.height,
      focused: true,
      debug_mode: initial_state.debug_mode,
      plugin_manager: initial_state.plugin_manager,
      plugin_manager_struct: Raxol.Plugins.Manager.new(),
      command_registry_table: initial_state.command_registry_table,
      rendering_engine: Map.get(initial_state, :rendering_engine),
      current_theme_id: safe_get_theme_id(),
      command_module: command_module,
      time_travel: Map.get(initial_state, :time_travel),
      cycle_profiler: Map.get(initial_state, :cycle_profiler)
    }

    send(runtime_pid, {:runtime_initialized, self()})

    send(runtime_pid, {:plugin_manager_ready, initial_state.plugin_manager})

    if test_env?(), do: send(self(), {:dispatcher_ready, self()})

    # Start app subscriptions (timers, event sources)
    state = setup_subscriptions(state)

    {:ok, state}
  end

  @doc """
  Dispatches an event to the appropriate handler based on event type and target.
  """
  def dispatch_event(event, state) do
    case Raxol.Core.ErrorHandling.safe_call(fn ->
           do_dispatch_event(event, state)
         end) do
      {:ok, result} ->
        result

      {:error, error} ->
        Raxol.Core.Runtime.Log.error_with_stacktrace(
          "Error dispatching event",
          error,
          nil,
          %{module: __MODULE__, event: event, state: state}
        )

        {:error, {:dispatch_error, error}, state}
    end
  end

  @doc """
  Handles an application-level event and updates the application state.
  """
  def handle_event(
        %Event{type: :mouse, data: %{action: :press, x: x, y: y}} = event,
        %State{} = state
      ) do
    case DispatcherHooks.hit_test(x, y, state.layout) do
      {:click, message} ->
        process_app_update(state, message, event)

      :miss ->
        do_handle_event(event, state)
    end
  end

  def handle_event(event, %State{} = state) do
    do_handle_event(event, state)
  end

  defp do_handle_event(event, state) do
    case try_bubble_event(event, state) do
      {:handled, {:message, message}} ->
        process_app_update(state, message, event)

      {:handled, _} ->
        send(state.runtime_pid, :render_needed)
        {:ok, state, []}

      {:commands, commands} ->
        context = build_command_context(state)
        process_commands(commands, context, state.command_module)
        send(state.runtime_pid, :render_needed)
        {:ok, state, commands}

      :passthrough ->
        # Pass the raw Event struct to update/2 — apps pattern-match on %Event{}
        process_app_update(state, event, event)
    end
  end

  defp try_bubble_event(_event, %State{view_tree: nil}), do: :passthrough

  defp try_bubble_event(event, state) do
    focused_id =
      if focus_manager_active?(),
        do: FocusManager.get_focused_element(),
        else: nil

    if focused_id do
      context = %{
        focused_element: focused_id,
        theme_id: state.current_theme_id
      }

      Bubbler.dispatch(event, state.view_tree, focused_id, context)
    else
      :passthrough
    end
  end

  defp process_app_update(state, message, event) do
    old_model = state.model

    {update_us, mem_before, mem_after, update_result} =
      DispatcherHooks.maybe_time_update(state.cycle_profiler, fn ->
        Application.delegate_update(state.app_module, message, state.model)
      end)

    case update_result do
      {updated_model, commands}
      when is_map(updated_model) and is_list(commands) ->
        DispatcherHooks.maybe_record_time_travel(
          state.time_travel,
          message,
          old_model,
          updated_model
        )

        DispatcherHooks.maybe_record_cycle_update(
          state.cycle_profiler,
          update_us,
          mem_before,
          mem_after,
          message
        )

        process_successful_update(state, updated_model, commands)

      {:error, reason} ->
        log_update_error(state, message, event, reason)

      other ->
        log_unexpected_return(state, message, event, other)
    end
  end

  defp process_successful_update(state, updated_model, commands) do
    context = build_command_context(state)
    process_commands(commands, context, state.command_module)

    updated_state = handle_theme_update(state, updated_model)
    send(state.runtime_pid, :render_needed)
    {:ok, updated_state, commands}
  end

  defp build_command_context(state) do
    %{
      pid: self(),
      command_registry_table: state.command_registry_table,
      runtime_pid: state.runtime_pid
    }
  end

  defp handle_theme_update(state, updated_model) do
    case Map.get(updated_model, :current_theme_id, state.current_theme_id) do
      same when same == state.current_theme_id ->
        %{state | model: updated_model}

      new_theme_id ->
        try do
          UserPreferences.set("theme.active_id", new_theme_id)
        catch
          :exit, _ -> :ok
        end

        %{state | model: updated_model, current_theme_id: new_theme_id}
    end
  end

  defp log_update_error(state, message, event, reason) do
    Raxol.Core.Runtime.Log.error_with_stacktrace(
      "Application update failed",
      reason,
      nil,
      %{
        module: __MODULE__,
        app_module: state.app_module,
        message: message,
        current_model: state.model,
        event: event
      }
    )

    {:error, reason}
  end

  defp log_unexpected_return(state, message, event, other) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "Unexpected return from #{state.app_module}.update",
      %{
        module: __MODULE__,
        app_module: state.app_module,
        message: message,
        current_model: state.model,
        event: event,
        other: other
      }
    )

    {:error, {:unexpected_return, other}}
  end

  @doc """
  Processes a system-level event that affects the runtime itself rather than the application logic.
  """
  def process_system_event(event, state) do
    case event do
      %Event{type: :resize, data: data} -> handle_resize_event(data, state)
      %Event{type: :quit} -> {:quit, state}
      %Event{type: :focus, data: data} -> handle_focus_event(data, state)
      %Event{type: :error, data: data} -> handle_error_event(data, state)
      _ -> {:ok, state, []}
    end
  end

  defp handle_resize_event(%{width: width, height: height}, state) do
    # Forward size to the Rendering Engine so layout uses actual terminal dimensions
    if state.rendering_engine do
      GenServer.cast(
        state.rendering_engine,
        {:update_size, %{width: width, height: height}}
      )
    end

    send(state.runtime_pid, :render_needed)
    {:ok, %{state | width: width, height: height}, []}
  end

  defp handle_focus_event(%{focused: focused}, state) do
    {:ok, %{state | focused: focused}, []}
  end

  defp handle_error_event(%{error: error}, state) do
    Raxol.Core.Runtime.Log.error_with_stacktrace(
      "System error event",
      error,
      nil,
      %{module: __MODULE__, error: error, state: state}
    )

    {:error, error, state}
  end

  # --- Public API for PubSub ---

  @doc "Subscribes the calling process to a specific event topic."
  @spec subscribe(atom()) :: {:ok, pid()}
  def subscribe(topic) when is_atom(topic) do
    Registry.register(@registry_name, topic, {})
  end

  @doc "Unsubscribes the calling process from a specific event topic."
  @spec unsubscribe(atom()) :: :ok
  def unsubscribe(topic) when is_atom(topic) do
    Registry.unregister(@registry_name, topic)
  end

  @doc "Broadcasts an event payload to all subscribers of a topic."
  @spec broadcast(atom(), map()) :: :ok
  def broadcast(topic, payload) when is_atom(topic) and is_map(payload) do
    Raxol.Core.Runtime.Log.debug(
      "[#{__MODULE__}] Broadcasting on topic #{topic}"
    )

    # Find subscribers for the topic (registry may not exist in web-only deployments)
    @registry_name
    |> Registry.lookup(topic)
    |> Enum.each(fn {pid, _value} ->
      send(pid, {:event, topic, payload})
    end)

    :ok
  rescue
    ArgumentError ->
      # Registry not started (web-only deployment); not an error
      :ok
  end

  # --- BaseManager Callbacks ---

  @impl true
  def handle_manager_cast(
        {:dispatch, {:agent_message, _from, _payload} = msg},
        state
      ) do
    Raxol.Core.Runtime.Log.debug(
      "[Dispatcher] handle_cast :dispatch agent_message: #{inspect(msg)}"
    )

    # Agent messages go directly to update/2, bypassing event/plugin pipeline
    dispatch_raw_message(msg, state)
  end

  @impl true
  def handle_manager_cast(
        {:dispatch, {:layout_recommendation, _rec} = msg},
        state
      ) do
    # Layout recommendations go directly to update/2
    dispatch_raw_message(msg, state)
  end

  @impl true
  def handle_manager_cast({:dispatch, event}, state) do
    Raxol.Core.Runtime.Log.debug(
      "[Dispatcher] handle_cast :dispatch event: #{inspect(event)}"
    )

    # Record input events for session recording (zero-coupling)
    DispatcherHooks.maybe_record_input(event)

    # Delegate to the main event handling logic using do_dispatch_event
    case do_dispatch_event(event, state) do
      {:ok, new_state, _commands} ->
        # Broadcast event globally if successfully handled by app logic
        # Ensure event.type and event.data are appropriate for broadcast
        broadcast_event_if_valid(event.type, event.data)

        {:noreply, new_state}

      {:quit, new_state} ->
        # Handle quit events by stopping the dispatcher
        {:stop, :normal, new_state}

      {:error, reason} ->
        Raxol.Core.Runtime.Log.error_with_stacktrace(
          "[Dispatcher] Error handling event in handle_cast",
          reason,
          nil,
          %{module: __MODULE__, event: event, state: state}
        )

        {:noreply, state}

      other ->
        Raxol.Core.Runtime.Log.warning_with_context(
          "[Dispatcher] Unexpected return from do_dispatch_event in handle_cast",
          %{module: __MODULE__, event: event, state: state, other: other}
        )

        {:noreply, state}
    end
  end

  @impl true
  def handle_manager_cast({:set_rendering_engine, pid}, state)
      when is_pid(pid) do
    # Forward current dimensions immediately — the initial resize event from the
    # Driver arrived before we had the rendering engine PID, so it was lost.
    if state.width > 0 and state.height > 0 do
      GenServer.cast(
        pid,
        {:update_size, %{width: state.width, height: state.height}}
      )
    end

    {:noreply, %{state | rendering_engine: pid}}
  end

  @impl true
  def handle_manager_cast({:internal_event, event}, state) do
    # This is for events that are internal to the dispatcher or runtime system.
    Raxol.Core.Runtime.Log.warning_with_context(
      "Dispatcher received unhandled internal_event",
      %{module: __MODULE__, event: event, state: state}
    )

    {:noreply, state}
  end

  # Catch-all for other cast messages
  @impl true
  def handle_manager_cast(
        {:update_plugin_manager, %Raxol.Plugins.Manager{} = updated},
        state
      ) do
    {:noreply, %{state | plugin_manager_struct: updated}}
  end

  @impl true
  def handle_manager_cast({:restore_model, model}, state) when is_map(model) do
    send(state.runtime_pid, :render_needed)
    {:noreply, %{state | model: model}}
  end

  @impl true
  def handle_manager_cast({:update_view_tree, view_tree}, state) do
    {:noreply, %{state | view_tree: view_tree}}
  end

  @impl true
  def handle_manager_cast({:update_layout, positioned_elements}, state) do
    {:noreply, %{state | layout: positioned_elements}}
  end

  @impl true
  def handle_manager_cast(msg, state) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "Dispatcher received unhandled cast message",
      %{module: __MODULE__, message: msg, state: state}
    )

    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:command_result, msg}, %State{} = state) do
    full_message = {:command_result, msg}
    process_command_result(state, full_message)
  end

  @impl true
  def handle_manager_info({:dispatcher_ready, _pid}, state) do
    # Acknowledge dispatcher initialization in test mode
    {:noreply, state}
  end

  @impl true
  def handle_manager_info({:subscription, msg}, state) do
    # Subscription timer fired — route message through app's update/2
    dispatch_raw_message(msg, state)
  end

  @impl true
  def handle_manager_info(msg, state) do
    Raxol.Core.Runtime.Log.warning_with_context(
      "Dispatcher received unhandled info message",
      %{module: __MODULE__, message: msg, state: state}
    )

    {:noreply, state}
  end

  defp process_command_result(state, message) do
    case Application.delegate_update(state.app_module, message, state.model) do
      {updated_model, commands}
      when is_map(updated_model) and is_list(commands) ->
        process_command_commands(state, updated_model, commands)

      {:error, reason} ->
        log_command_failure(
          :error,
          "[Dispatcher] Error calling delegate_update in handle_info",
          reason,
          %{module: __MODULE__, msg: message, state: state}
        )

        {:noreply, state}

      other ->
        log_command_failure(
          :warning,
          "[Dispatcher] Unexpected return from delegate_update in handle_info",
          other,
          %{module: __MODULE__, msg: message, state: state, other: other}
        )

        {:noreply, state}
    end
  end

  defp process_command_commands(state, updated_model, commands) do
    context = build_command_context(state)

    case Raxol.Core.ErrorHandling.safe_call(fn ->
           process_commands(commands, context, state.command_module)
         end) do
      {:ok, _} ->
        :ok

      {:error, error} ->
        log_command_failure(
          :error,
          "[Dispatcher] Error processing commands from command result",
          error,
          %{module: __MODULE__}
        )
    end

    {:noreply, %{state | model: updated_model}}
  end

  defp log_command_failure(:error, label, reason, context) do
    Raxol.Core.Runtime.Log.error_with_stacktrace(label, reason, nil, context)
  end

  defp log_command_failure(:warning, label, _reason, context) do
    Raxol.Core.Runtime.Log.warning_with_context(label, context)
  end

  @impl true
  def handle_manager_call(:get_plugin_manager, _from, state) do
    {:reply, {:ok, state.plugin_manager_struct}, state}
  end

  @impl true
  def handle_manager_call(:get_model, _from, state) do
    {:reply, {:ok, state.model}, state}
  end

  @impl true
  def handle_manager_call(:get_view_tree, _from, state) do
    {:reply, {:ok, state.view_tree}, state}
  end

  @impl true
  def handle_manager_call(:get_render_context, _from, state) do
    Raxol.Core.Runtime.Log.debug(
      "Dispatcher received :get_render_context call for #{inspect(state.app_module)}"
    )

    focused_element =
      if focus_manager_active?(),
        do: FocusManager.get_focused_element(),
        else: nil

    reduced_motion =
      try do
        if Code.ensure_loaded?(Raxol.Animation.Framework) do
          Raxol.Animation.Framework.should_reduce_motion?()
        else
          false
        end
      catch
        :exit, _ -> false
      end

    render_context = %{
      model: state.model,
      theme_id: state.current_theme_id,
      focused_element: focused_element,
      reduced_motion: reduced_motion
    }

    Raxol.Core.Runtime.Log.debug(
      "Dispatcher returning render context: #{inspect(render_context)}"
    )

    {:reply, {:ok, render_context}, state}
  end

  @impl true
  def terminate(reason, _state) do
    Raxol.Core.Runtime.Log.info(
      "Event Dispatcher terminating. Reason: #{inspect(reason)}"
    )

    :ok
  end

  defp do_dispatch_event(event, state) do
    log_debug_if_enabled(state.debug_mode, event)
    route_event_by_type(system_event?(event), event, state)
  end

  defp system_event?(%Event{type: type}) do
    type in [:resize, :quit, :focus, :error, :system]
  end

  defp system_event?(_), do: false

  defp apply_plugin_filters(event, state) do
    manager_pid = state.plugin_manager

    if is_pid(manager_pid) and Process.alive?(manager_pid) do
      case GenServer.call(manager_pid, {:filter_event, event}) do
        {:ok, filtered_event} -> filtered_event
        :halt -> nil
        {:error, _reason} -> nil
        _ -> event
      end
    else
      event
    end
  rescue
    e ->
      Logger.debug("Plugin filter failed: #{Exception.message(e)}")
      event
  end

  # --- Command Processing ---

  # --- Helper Functions for Pattern Matching ---

  defp safe_get_theme_id do
    UserPreferences.get_theme_id()
  catch
    :exit, _ -> :default
  end

  # Route a raw message (not an Event struct) directly through update/2
  defp dispatch_raw_message(msg, state) do
    case process_app_update(state, msg, msg) do
      {:ok, new_state, _commands} ->
        {:noreply, new_state}

      _ ->
        {:noreply, state}
    end
  end

  defp setup_subscriptions(state) do
    try do
      if function_exported?(state.app_module, :subscribe, 1) do
        case state.app_module.subscribe(state.model) do
          subscriptions when is_list(subscriptions) ->
            Enum.each(subscriptions, fn
              %Raxol.Core.Runtime.Subscription{} = sub ->
                Raxol.Core.Runtime.Subscription.start(sub, %{pid: self()})

              _ ->
                :ok
            end)

          _ ->
            :ok
        end
      end
    rescue
      e ->
        Logger.debug("Subscription setup failed: #{Exception.message(e)}")
        :ok
    end

    state
  end

  defp broadcast_event_if_valid(event_type, event_data)
       when is_atom(event_type) and is_map(event_data) do
    Raxol.Core.Runtime.Log.debug(
      "[Dispatcher] Broadcasting event: #{inspect(event_type)} via internal broadcast"
    )

    _ = __MODULE__.broadcast(event_type, event_data)
  end

  defp broadcast_event_if_valid(event_type, event_data) do
    Raxol.Core.Runtime.Log.warning(
      "[Dispatcher] Event not broadcast due to invalid type/data: type=#{inspect(event_type)}, data=#{inspect(event_data)}"
    )
  end

  defp log_debug_if_enabled(true, event) do
    Raxol.Core.Runtime.Log.debug("Dispatching event: #{inspect(event)}")
  end

  defp log_debug_if_enabled(false, _event), do: :ok

  defp route_event_by_type(true, event, state) do
    process_system_event(event, state)
  end

  defp route_event_by_type(false, event, state) do
    filtered_event = apply_plugin_filters(event, state)
    handle_filtered_event(filtered_event, state)
  end

  defp handle_filtered_event(nil, state), do: {:ok, state, []}

  defp handle_filtered_event(filtered_event, state) do
    case maybe_handle_focus_navigation(filtered_event, state) do
      {:handled, result} -> result
      :pass -> handle_event(filtered_event, state)
    end
  end

  defp maybe_handle_focus_navigation(
         %Event{type: :key, data: %{key: :tab} = data},
         state
       ) do
    if focus_manager_active?() do
      shift = Map.get(data, :shift, false)
      old_focus = FocusManager.get_focused_element()

      result =
        if shift,
          do: FocusManager.focus_previous(),
          else: FocusManager.focus_next()

      case result do
        {:ok, new_focus_id} ->
          {:handled,
           process_app_update(
             state,
             {:focus_changed, old_focus, new_focus_id},
             nil
           )}

        {:error, _} ->
          :pass
      end
    else
      :pass
    end
  end

  defp maybe_handle_focus_navigation(_event, _state), do: :pass

  defp focus_manager_active? do
    Process.whereis(Raxol.Core.FocusManager.FocusServer) != nil
  end

  # --- Command Processing ---

  defp process_commands(commands, context, command_module)
       when is_list(commands) do
    Raxol.Core.Runtime.Log.debug(
      "[Dispatcher.process_commands] Processing commands: #{inspect(commands)} with context: #{inspect(context)}"
    )

    Enum.each(commands, fn command ->
      case command do
        %Command{} = cmd ->
          command_module.execute(cmd, context)

        _ ->
          Raxol.Core.Runtime.Log.warning_with_context(
            "[#{__MODULE__}] Invalid command format: #{inspect(command)}. Expected %Raxol.Core.Runtime.Command{}. Ignoring.",
            %{command: command}
          )
      end
    end)
  end

  defp test_env?, do: Code.ensure_loaded?(Mix) and Mix.env() == :test
end
