defmodule Raxol.UI.Rendering.AdaptiveFramerate do
  @moduledoc """
  Dynamically adjusts rendering frame rate based on system performance and content complexity.

  ## Frame Rate Targets
  - High complexity: 30fps (33ms) - Complex layouts, many elements
  - Medium complexity: 45fps (22ms) - Moderate layouts  
  - Low complexity: 60fps (16ms) - Simple layouts, minimal changes
  - Static content: 10fps (100ms) - No active animations

  ## Adaptation Triggers
  - Render time exceeding target
  - CPU usage above threshold
  - Large number of damage regions
  - Complex tree structures
  """

  use Raxol.Core.Behaviours.BaseManager

  require Logger
  require Raxol.Core.Runtime.Log

  @fps_60 16
  @fps_45 22
  @fps_30 33
  @fps_10 100

  @high_cpu_threshold 80.0
  @complex_tree_threshold 100

  # Accessor functions for module attributes (needed for nested modules)
  def fps_60, do: @fps_60

  defmodule FramerateState do
    @moduledoc false

    # 60fps interval in ms
    @fps_60 16

    defstruct current_interval_ms: @fps_60,
              target_fps: 60,
              render_times: [],
              cpu_samples: [],
              complexity_history: [],
              adaptation_timer_ref: nil,
              stats: %{
                adaptations: 0,
                avg_render_time: 0.0,
                avg_cpu_usage: 0.0
              }
  end

  # Public API

  @doc """
  Reports a completed render with timing and complexity metrics.
  Used to adapt frame rate based on actual performance.
  """
  @spec report_render(
          render_time_us :: non_neg_integer(),
          complexity_score :: non_neg_integer(),
          damage_count :: non_neg_integer(),
          pid() | atom()
        ) :: :ok
  def report_render(
        render_time_us,
        complexity_score,
        damage_count,
        manager \\ __MODULE__
      ) do
    GenServer.cast(
      manager,
      {:report_render, render_time_us, complexity_score, damage_count,
       System.monotonic_time(:millisecond)}
    )
  end

  @doc """
  Gets the current recommended frame interval in milliseconds.
  """
  @spec get_frame_interval(pid() | atom()) :: pos_integer()
  def get_frame_interval(manager \\ __MODULE__) do
    GenServer.call(manager, :get_frame_interval)
  end

  @doc """
  Gets current framerate statistics and adaptation state.
  """
  @spec get_stats(pid() | atom()) :: map()
  def get_stats(manager \\ __MODULE__) do
    GenServer.call(manager, :get_stats)
  end

  @doc """
  Forces a frame rate adaptation check.
  Useful for testing or immediate response to system changes.
  """
  @spec force_adaptation(pid() | atom()) :: :ok
  def force_adaptation(manager \\ __MODULE__) do
    GenServer.cast(manager, :force_adaptation)
  end

  # BaseManager Implementation

  @impl Raxol.Core.Behaviours.BaseManager
  def init_manager(_opts) do
    # Start unified timer manager for adaptation checks
    state =
      case Raxol.UI.Rendering.TimerServer.start_adaptive_timer(self(), 1000) do
        :ok ->
          %FramerateState{
            adaptation_timer_ref: :timer_server
          }

        {:error, :not_started} ->
          # Fallback to Process.send_after in test mode when TimerServer isn't running
          timer_ref = Process.send_after(self(), :adapt_framerate, 1000)

          %FramerateState{
            adaptation_timer_ref: timer_ref
          }

        error ->
          Raxol.Core.Runtime.Log.warning(
            "AdaptiveFramerate: Failed to start timer - #{inspect(error)}"
          )

          %FramerateState{
            adaptation_timer_ref: nil
          }
      end

    Raxol.Core.Runtime.Log.debug("AdaptiveFramerate: Started with 60fps target")

    {:ok, state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_cast(
        {:report_render, render_time_us, complexity_score, damage_count,
         _timestamp},
        state
      ) do
    # Update render time history (keep last 10 samples)
    new_render_times =
      [render_time_us | state.render_times]
      |> Enum.take(10)

    # Update complexity history
    total_complexity = complexity_score + damage_count

    new_complexity_history =
      [total_complexity | state.complexity_history]
      |> Enum.take(10)

    # Sample CPU usage (simplified - in real implementation would use :cpu_sup)
    cpu_usage = sample_cpu_usage()

    new_cpu_samples =
      [cpu_usage | state.cpu_samples]
      |> Enum.take(10)

    new_state = %{
      state
      | render_times: new_render_times,
        complexity_history: new_complexity_history,
        cpu_samples: new_cpu_samples
    }

    {:noreply, new_state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_cast(:force_adaptation, state) do
    new_state = perform_adaptation(state)
    {:noreply, new_state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_call(:get_frame_interval, _from, state) do
    {:reply, state.current_interval_ms, state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_call(:get_stats, _from, state) do
    stats =
      Map.merge(state.stats, %{
        current_fps: fps_from_interval(state.current_interval_ms),
        current_interval_ms: state.current_interval_ms,
        target_fps: state.target_fps,
        sample_count: length(state.render_times)
      })

    {:reply, stats, state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_info({:adaptive_frame_tick}, state) do
    # Perform adaptation check
    new_state = perform_adaptation(state)

    # Timer is already running via unified timer manager, no need to reschedule
    final_state = new_state

    {:noreply, final_state}
  end

  # Private Helper Functions

  defp perform_adaptation(state) do
    # Skip adaptation if we don't have enough samples
    if length(state.render_times) < 3 do
      state
    else
      # Calculate metrics
      avg_render_time = average(state.render_times)
      avg_complexity = average(state.complexity_history)
      avg_cpu_usage = average(state.cpu_samples)

      # Determine optimal frame rate
      new_target_fps =
        determine_optimal_fps(avg_render_time, avg_complexity, avg_cpu_usage)

      new_interval_ms = interval_from_fps(new_target_fps)

      # Only adapt if there's a significant change
      if abs(new_interval_ms - state.current_interval_ms) > 2 do
        log_adaptation(
          state.target_fps,
          new_target_fps,
          avg_render_time,
          avg_complexity,
          avg_cpu_usage
        )

        # Update stats
        new_stats = %{
          state.stats
          | adaptations: state.stats.adaptations + 1,
            # Convert to ms
            avg_render_time: avg_render_time / 1000.0,
            avg_cpu_usage: avg_cpu_usage
        }

        %{
          state
          | current_interval_ms: new_interval_ms,
            target_fps: new_target_fps,
            stats: new_stats
        }
      else
        state
      end
    end
  end

  defp determine_optimal_fps(avg_render_time_us, avg_complexity, avg_cpu_usage) do
    cond do
      # If render time is too high, reduce FPS
      # >25ms render time
      avg_render_time_us > 25_000 ->
        30

      # If CPU usage is high, reduce FPS  
      avg_cpu_usage > @high_cpu_threshold ->
        30

      # If complexity is very high, reduce FPS
      avg_complexity > @complex_tree_threshold * 2 ->
        30

      # Medium complexity scenarios
      avg_complexity > @complex_tree_threshold or avg_render_time_us > 15_000 ->
        45

      # High performance scenarios - check if we can go to 60fps
      avg_render_time_us < 8_000 and avg_cpu_usage < 50.0 and
          avg_complexity < 50 ->
        60

      # Default to 45fps for most scenarios
      true ->
        45
    end
  end

  defp sample_cpu_usage do
    # Simplified CPU sampling - in real implementation would use proper system metrics
    # For now, return a random value that simulates realistic CPU usage
    :rand.uniform(100)
  end

  defp average([]), do: 0.0

  defp average(list) do
    Enum.sum(list) / length(list)
  end

  defp fps_from_interval(interval_ms) do
    round(1000 / interval_ms)
  end

  defp interval_from_fps(60), do: @fps_60
  defp interval_from_fps(45), do: @fps_45
  defp interval_from_fps(30), do: @fps_30
  defp interval_from_fps(10), do: @fps_10
  defp interval_from_fps(fps) when fps > 0, do: round(1000 / fps)

  defp log_adaptation(
         old_fps,
         new_fps,
         avg_render_time,
         avg_complexity,
         avg_cpu_usage
       ) do
    Raxol.Core.Runtime.Log.info(
      "AdaptiveFramerate: #{old_fps}fps -> #{new_fps}fps " <>
        "(render: #{Float.round(avg_render_time / 1000, 1)}ms, " <>
        "complexity: #{round(avg_complexity)}, " <>
        "cpu: #{Float.round(avg_cpu_usage, 1)}%)"
    )
  end
end
