defmodule Raxol.Terminal.Cache.System do
  @moduledoc """
  Unified caching system for the Raxol terminal emulator.
  This module provides a centralized caching mechanism that consolidates all caching
  operations across the terminal system, including:
  - Buffer caching
  - Animation caching
  - Scroll caching
  - Clipboard caching
  - General purpose caching
  """

  use Raxol.Core.Behaviours.BaseManager
  alias Raxol.Terminal.Cache.EvictionHelpers

  @type namespace :: :buffer | :animation | :scroll | :clipboard | :general
  @type cache_key :: term()
  @type cache_value :: term()
  @type cache_entry :: %{
          value: cache_value(),
          size: non_neg_integer(),
          created_at: integer(),
          last_access: integer(),
          access_count: non_neg_integer(),
          ttl: integer() | nil,
          metadata: map()
        }
  @type cache_stats :: %{
          size: non_neg_integer(),
          max_size: non_neg_integer(),
          hit_count: non_neg_integer(),
          miss_count: non_neg_integer(),
          hit_ratio: float(),
          eviction_count: non_neg_integer()
        }

  # BaseManager provides start_link

  @doc """
  Gets a value from the cache.

  ## Parameters
    * `key` - The cache key
    * `opts` - Get options
      * `:namespace` - Cache namespace (default: :general)
  """
  def get(key, opts \\ []) do
    namespace = Keyword.get(opts, :namespace, :general)
    GenServer.call(__MODULE__, {:get, namespace, key})
  end

  @doc """
  Puts a value in the cache.

  ## Parameters
    * `key` - The cache key
    * `value` - The value to cache
    * `opts` - Put options
      * `:namespace` - Cache namespace (default: :general)
      * `:ttl` - Time-to-live in seconds
      * `:metadata` - Additional metadata
  """
  def put(key, value, opts \\ []) do
    namespace = Keyword.get(opts, :namespace, :general)
    ttl = Keyword.get(opts, :ttl)
    metadata = Keyword.get(opts, :metadata, %{})
    GenServer.call(__MODULE__, {:put, namespace, key, value, ttl, metadata})
  end

  @doc """
  Invalidates a cache entry.

  ## Parameters
    * `key` - The cache key
    * `opts` - Invalidate options
      * `:namespace` - Cache namespace (default: :general)
  """
  def invalidate(key, opts \\ []) do
    namespace = Keyword.get(opts, :namespace, :general)
    GenServer.call(__MODULE__, {:invalidate, namespace, key})
  end

  @doc """
  Gets cache statistics.

  ## Parameters
    * `opts` - Stats options
      * `:namespace` - Cache namespace (default: :general)
  """
  def stats(opts \\ []) do
    namespace = Keyword.get(opts, :namespace, :general)
    GenServer.call(__MODULE__, {:stats, namespace})
  end

  @doc """
  Clears the cache.

  ## Parameters
    * `opts` - Clear options
      * `:namespace` - Cache namespace (default: :general)
  """
  def clear(opts \\ []) do
    namespace = Keyword.get(opts, :namespace, :general)
    GenServer.call(__MODULE__, {:clear, namespace})
  end

  @doc """
  Returns the current monotonic time in milliseconds.
  This is used for cache expiration and timing operations.
  """
  @spec monotonic_time() :: integer()
  def monotonic_time do
    System.monotonic_time(:millisecond)
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def init_manager(opts) do
    max_size = Keyword.get(opts, :max_size, Raxol.Core.Defaults.cache_max_bytes())
    default_ttl = Keyword.get(opts, :default_ttl, Raxol.Core.Defaults.cache_ttl_seconds())
    eviction_policy = Keyword.get(opts, :eviction_policy, :lru)
    compression_enabled = Keyword.get(opts, :compression_enabled, true)
    namespace_configs = Keyword.get(opts, :namespace_configs, %{})

    namespaces = initialize_namespaces(namespace_configs, max_size)

    state = %{
      namespaces: namespaces,
      default_ttl: default_ttl,
      eviction_policy: eviction_policy,
      compression_enabled: compression_enabled
    }

    {:ok, state}
  end

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_call({:get, namespace, key}, _from, state) do
    case get_namespace(state, namespace) do
      nil ->
        {:reply, {:error, :namespace_not_found}, state}

      namespace_state ->
        handle_cache_entry(namespace_state, key, state, namespace)
    end
  end

  def handle_manager_call(
        {:put, namespace, key, value, ttl, metadata},
        _from,
        state
      ) do
    case get_namespace(state, namespace) do
      nil ->
        {:reply, {:error, :namespace_not_found}, state}

      namespace_state ->
        entry_size = calculate_size(value)

        {updated_cache, updated_size} =
          case namespace_state.size + entry_size > namespace_state.max_size do
            true ->
              {cache_after_eviction, size_after_eviction} =
                evict_entries(
                  namespace_state.cache,
                  namespace_state.size,
                  entry_size,
                  state.eviction_policy,
                  namespace_state.max_size
                )

              {cache_after_eviction, size_after_eviction}

            false ->
              {namespace_state.cache, namespace_state.size}
          end

        entry = %{
          value: value,
          size: entry_size,
          created_at: System.system_time(:second),
          last_access: System.monotonic_time(:millisecond),
          access_count: 1,
          ttl: ttl || state.default_ttl,
          metadata: metadata
        }

        updated_cache = Map.put(updated_cache, key, entry)
        updated_size = updated_size + entry_size

        updated_state =
          update_namespace(state, namespace, %{
            namespace_state
            | cache: updated_cache,
              size: updated_size
          })

        {:reply, :ok, updated_state}
    end
  end

  def handle_manager_call({:invalidate, namespace, key}, _from, state) do
    case get_namespace(state, namespace) do
      nil ->
        {:reply, {:error, :namespace_not_found}, state}

      namespace_state ->
        case Map.pop(namespace_state.cache, key) do
          {nil, _} ->
            {:reply, :ok, state}

          {entry, updated_cache} ->
            updated_size = namespace_state.size - entry.size

            updated_state =
              update_namespace(state, namespace, %{
                namespace_state
                | cache: updated_cache,
                  size: updated_size
              })

            {:reply, :ok, updated_state}
        end
    end
  end

  def handle_manager_call({:stats, namespace}, _from, state) do
    case get_namespace(state, namespace) do
      nil ->
        {:reply, {:error, :namespace_not_found}, state}

      namespace_state ->
        stats = %{
          size: namespace_state.size,
          max_size: namespace_state.max_size,
          hit_count: namespace_state.hit_count,
          miss_count: namespace_state.miss_count,
          hit_ratio: calculate_hit_ratio(namespace_state),
          eviction_count: namespace_state.eviction_count
        }

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

  def handle_manager_call({:clear, namespace}, _from, state) do
    case get_namespace(state, namespace) do
      nil ->
        {:reply, {:error, :namespace_not_found}, state}

      namespace_state ->
        updated_state =
          update_namespace(state, namespace, %{
            namespace_state
            | cache: %{},
              size: 0,
              hit_count: 0,
              miss_count: 0,
              eviction_count: 0
          })

        {:reply, :ok, updated_state}
    end
  end

  defp handle_cache_entry(namespace_state, key, state, namespace) do
    case Map.get(namespace_state.cache, key) do
      nil ->
        updated_state =
          update_namespace(state, namespace, %{
            namespace_state
            | miss_count: namespace_state.miss_count + 1
          })

        {:reply, {:error, :not_found}, updated_state}

      entry ->
        case expired?(entry) do
          true ->
            handle_expired_entry(entry, key, namespace_state, state, namespace)

          false ->
            handle_valid_entry(entry, key, namespace_state, state, namespace)
        end
    end
  end

  defp handle_expired_entry(entry, key, namespace_state, state, namespace) do
    updated_cache = Map.delete(namespace_state.cache, key)
    updated_size = namespace_state.size - entry.size

    updated_state =
      update_namespace(state, namespace, %{
        namespace_state
        | cache: updated_cache,
          size: updated_size,
          miss_count: namespace_state.miss_count + 1
      })

    {:reply, {:error, :expired}, updated_state}
  end

  defp handle_valid_entry(entry, key, namespace_state, state, namespace) do
    updated_entry = %{
      entry
      | last_access: System.monotonic_time(:millisecond),
        access_count: entry.access_count + 1
    }

    updated_cache = Map.put(namespace_state.cache, key, updated_entry)

    updated_state =
      update_namespace(state, namespace, %{
        namespace_state
        | cache: updated_cache,
          hit_count: namespace_state.hit_count + 1
      })

    {:reply, {:ok, entry.value}, updated_state}
  end

  defp initialize_namespaces(configs, default_max_size) do
    default_namespace = %{
      cache: %{},
      size: 0,
      max_size: default_max_size,
      hit_count: 0,
      miss_count: 0,
      eviction_count: 0
    }

    [:buffer, :animation, :scroll, :clipboard, :general]
    |> Enum.reduce(%{}, fn namespace, acc ->
      config = Map.get(configs, namespace, %{})
      max_size = Map.get(config, :max_size, default_max_size)
      namespace_state = Map.put(default_namespace, :namespace, namespace)
      Map.put(acc, namespace, %{namespace_state | max_size: max_size})
    end)
  end

  defp get_namespace(state, namespace) do
    Map.get(state.namespaces, namespace)
  end

  defp update_namespace(state, namespace, namespace_state) do
    %{state | namespaces: Map.put(state.namespaces, namespace, namespace_state)}
  end

  defp expired?(entry) do
    case entry.ttl do
      nil -> false
      ttl -> System.system_time(:second) - entry.created_at > ttl
    end
  end

  defp calculate_size(value) do
    :erlang.external_size(value)
  end

  defp calculate_hit_ratio(namespace_state) do
    total = namespace_state.hit_count + namespace_state.miss_count

    case total > 0 do
      true -> namespace_state.hit_count / total
      false -> 0.0
    end
  end

  defp evict_entries(cache, current_size, needed_size, policy, max_size) do
    case policy do
      :lru ->
        EvictionHelpers.evict_lru(cache, current_size, needed_size, max_size)

      :lfu ->
        EvictionHelpers.evict_lfu(cache, current_size, needed_size, max_size)

      :fifo ->
        EvictionHelpers.evict_fifo(cache, current_size, needed_size, max_size)
    end
  end
end
