defmodule Raxol.Core.Performance.Optimizer do
  @moduledoc """
  Refactored Performance optimization strategies with GenServer-based memoization.

  This module provides the same optimization techniques as the original but uses
  supervised state management instead of Process dictionary for memoization.

  ## Migration Notes

  The memoize macro now uses Raxol.Core.Performance.Memoization.Server instead
  of Process dictionary, providing better fault tolerance and monitoring.
  """

  import Raxol.Core.Performance.Profiler
  alias Raxol.Core.Runtime.Log

  @doc """
  Optimizes database queries to prevent N+1 problems.

  ## Examples

      # Before optimization
      users = Repo.all(User)
      users_with_posts = Enum.map(users, fn user ->
        %{user | posts: Repo.all(assoc(user, :posts))}
      end)

      # After optimization
      users_with_posts = optimize_query do
        User |> preload(:posts) |> Repo.all()
      end
  """
  defmacro optimize_query(do: query) do
    quote do
      # Profile the query first
      profile :database_query, metadata: %{optimized: true} do
        unquote(query)
      end
    end
  end

  @doc """
  Implements caching for expensive operations.

  ## Options

  - `:ttl` - Time to live in milliseconds (default: 60_000)
  - `:key` - Cache key (required)
  - `:refresh` - Whether to refresh cache on hit (default: false)

  ## Examples

      cached :expensive_calculation, key: "calc_123", ttl: 300_000 do
        perform_expensive_calculation(123)
      end
  """
  defmacro cached(operation, opts, do: block) do
    quote do
      Raxol.Core.Performance.Optimizer.execute_with_cache(
        unquote(operation),
        unquote(opts),
        fn -> unquote(block) end
      )
    end
  end

  @doc """
  Implements lazy loading for large datasets.

  ## Examples

      lazy_stream :large_file_reader do
        File.stream!("large_file.txt")
        |> Stream.map(&process_line/1)
      end
  """
  defmacro lazy_stream(operation, do: stream) do
    quote do
      profile unquote(operation), metadata: %{lazy: true} do
        unquote(stream)
      end
    end
  end

  @doc """
  Memoizes function results to avoid recomputation.

  Now uses GenServer-based caching instead of Process dictionary.

  ## Examples

      defmodule Calculator do
        use Raxol.Core.Performance.Optimizer
  alias Raxol.Core.Runtime.Log

        memoize expensive_calculation(n) do
          # Complex calculation
          factorial(n) * fibonacci(n)
        end
      end
  """
  defmacro memoize({name, _, args} = _call, do: body) do
    key = {name, args}

    quote do
      Raxol.Core.Performance.Optimizer.ensure_memoization_server()

      Raxol.Core.Performance.Memoization.Server.get_or_compute(
        unquote(key),
        fn -> unquote(body) end
      )
    end
  end

  @doc """
  Optimizes concurrent operations using Task.async_stream.

  ## Options

  - `:max_concurrency` - Maximum concurrent tasks (default: System.schedulers_online())
  - `:timeout` - Task timeout in milliseconds (default: 5000)
  - `:ordered` - Maintain order of results (default: true)

  ## Examples

      concurrent_map users, &send_email/1, max_concurrency: 10
  """
  def concurrent_map(enumerable, fun, opts \\ []) do
    max_concurrency =
      Keyword.get(opts, :max_concurrency, System.schedulers_online())

    timeout = Keyword.get(opts, :timeout, 5000)
    ordered = Keyword.get(opts, :ordered, true)

    profile :concurrent_operation, metadata: %{concurrency: max_concurrency} do
      enumerable
      |> Task.async_stream(fun,
        max_concurrency: max_concurrency,
        timeout: timeout,
        ordered: ordered
      )
      |> Enum.map(fn
        {:ok, result} -> result
        {:exit, reason} -> {:error, reason}
      end)
    end
  end

  @doc """
  Batches operations to reduce overhead.

  ## Examples

      batch_process records, batch_size: 100 do |batch|
        Repo.insert_all(Record, batch)
      end
  """
  def batch_process(enumerable, opts, fun) do
    batch_size = Keyword.get(opts, :batch_size, 100)

    enumerable
    |> Stream.chunk_every(batch_size)
    |> Enum.each(fn batch ->
      profile :batch_operation, metadata: %{batch_size: length(batch)} do
        fun.(batch)
      end
    end)
  end

  @doc """
  Optimizes string concatenation for better performance.

  ## Examples

      # Instead of multiple concatenations
      result = str1 <> str2 <> str3 <> str4

      # Use
      result = string_builder([str1, str2, str3, str4])
  """
  def string_builder(parts) when is_list(parts) do
    profile :string_building, metadata: %{parts: length(parts)} do
      IO.iodata_to_binary(parts)
    end
  end

  @doc """
  Implements circuit breaker pattern for external calls.
  """
  def with_circuit_breaker(name, fun, opts \\ []) do
    profile :circuit_breaker_call, metadata: %{circuit: name} do
      Raxol.Core.ErrorRecovery.with_circuit_breaker(name, fun, opts)
    end
  end

  @doc """
  Optimizes list operations using appropriate data structures.

  ## Examples

      # For frequent prepends, use lists
      optimize_list_ops :prepend, initial_list do |list|
        [new_item | list]
      end

      # For frequent lookups, convert to map
      optimize_list_ops :lookup, list do |list|
        Map.new(list, & {&1.id, &1})
      end
  """
  def optimize_list_ops(operation_type, data, transformer) do
    profile :"list_ops_#{operation_type}",
      metadata: %{size: length_or_size(data)} do
      transformer.(data)
    end
  end

  @doc """
  Reduces memory usage by implementing streaming where possible.

  ## Examples

      stream_process "large_file.csv" do |line|
        parse_csv_line(line)
        |> process_record()
      end
  """
  def stream_process(file_path, processor) do
    profile :stream_processing, metadata: %{file: file_path} do
      File.stream!(file_path)
      |> Stream.map(processor)
      |> Stream.run()
    end
  end

  @doc """
  Optimizes ETS table operations.

  ## Examples

      ets_batch_insert(:my_table, records, batch_size: 1000)
  """
  def ets_batch_insert(table, records, opts \\ []) do
    batch_size = Keyword.get(opts, :batch_size, 1000)

    records
    |> Stream.chunk_every(batch_size)
    |> Enum.each(fn batch ->
      profile :ets_batch_insert, metadata: %{table: table, size: length(batch)} do
        :ets.insert(table, batch)
      end
    end)
  end

  @doc """
  Implements connection pooling for external resources.

  Uses poolboy if available, otherwise creates a simple connection pool using Registry.

  ## Examples

      with_pooled_connection(:database, fn conn ->
        MyRepo.query(conn, "SELECT * FROM users")
      end)
  """
  def with_pooled_connection(pool_name, fun) when is_function(fun, 1) do
    profile :pooled_connection, metadata: %{pool: pool_name} do
      case poolboy_available?() do
        true ->
          :poolboy.transaction(pool_name, fun)

        false ->
          # Fallback to simple connection management
          case get_or_create_connection(pool_name) do
            {:ok, conn} ->
              case Raxol.Core.ErrorHandling.safe_call(fn -> fun.(conn) end) do
                {:ok, result} ->
                  return_connection(pool_name, conn)
                  result

                {:error, reason} ->
                  return_connection(pool_name, conn)
                  {:error, reason}
              end

            {:error, reason} ->
              Log.warning(
                "Failed to get connection from pool #{pool_name}: #{inspect(reason)}"
              )

              {:error, :connection_unavailable}
          end
      end
    end
  end

  def with_pooled_connection(pool_name, fun) when is_function(fun, 0) do
    # For compatibility with zero-arity functions
    profile :pooled_connection, metadata: %{pool: pool_name} do
      case poolboy_available?() do
        true -> :poolboy.transaction(pool_name, fn _conn -> fun.() end)
        false -> fun.()
      end
    end
  end

  @doc """
  Initializes a connection pool with the given configuration.

  ## Options

  - `:size` - Pool size (default: 5)
  - `:max_overflow` - Maximum overflow connections (default: 10) 
  - `:worker` - Worker module for connections
  - `:worker_args` - Arguments for worker initialization

  ## Examples

      init_connection_pool(:database, 
        size: 5, 
        max_overflow: 10,
        worker: MyConnectionWorker,
        worker_args: [host: "localhost", port: 5432]
      )
  """
  def init_connection_pool(pool_name, opts \\ []) do
    case poolboy_available?() do
      true -> init_poolboy_pool(pool_name, opts)
      false -> init_simple_pool(pool_name, opts)
    end
  end

  @doc """
  Optimizes GenServer calls by batching.

  ## Examples

      batch_genserver_calls(MyServer, messages, batch_size: 50)
  """
  def batch_genserver_calls(server, messages, opts \\ []) do
    batch_size = Keyword.get(opts, :batch_size, 50)
    timeout = Keyword.get(opts, :timeout, 5000)

    messages
    |> Stream.chunk_every(batch_size)
    |> Enum.map(fn batch ->
      profile :batched_genserver_call,
        metadata: %{server: server, batch_size: length(batch)} do
        GenServer.call(server, {:batch, batch}, timeout)
      end
    end)
  end

  @doc """
  Implements read-through cache pattern.
  """
  def read_through_cache(key, loader, opts \\ []) do
    ttl = Keyword.get(opts, :ttl, 60_000)
    cache_name = Keyword.get(opts, :cache, :default_cache)

    case lookup_cache(cache_name, key) do
      {:ok, value} ->
        profile :cache_hit, metadata: %{key: key} do
          value
        end

      :miss ->
        profile :cache_miss, metadata: %{key: key} do
          value = loader.()
          store_cache(cache_name, key, value, ttl)
          value
        end
    end
  end

  @doc """
  Optimizes recursive operations using tail recursion.

  ## Examples

      # Convert recursive function to tail-recursive
      def sum([]), do: 0
      def sum([h | t]), do: h + sum(t)

      # Becomes
      tail_recursive_sum(list)
  """
  def tail_recursive_sum(list), do: do_sum(list, 0)
  @spec do_sum(any(), any()) :: any()
  defp do_sum([], acc), do: acc
  @spec do_sum(any(), any()) :: any()
  defp do_sum([h | t], acc), do: do_sum(t, acc + h)

  @doc """
  Profiles and suggests algorithm improvements.
  """
  def analyze_algorithm(name, implementations) do
    results =
      Enum.map(implementations, fn {impl_name, fun} ->
        result =
          benchmark(:"#{name}_#{impl_name}", iterations: 1000) do
            fun.()
          end

        {impl_name, result}
      end)

    best =
      results
      |> Enum.min_by(fn {_, stats} -> stats.mean end)
      |> elem(0)

    %{
      results: results,
      recommendation: "Use #{best} implementation",
      improvement_potential: calculate_improvement_potential(results)
    }
  end

  def execute_with_cache(operation, opts, fun) do
    key = Keyword.fetch!(opts, :key)
    ttl = Keyword.get(opts, :ttl, 60_000)
    refresh = Keyword.get(opts, :refresh, false)

    cache_key = {operation, key}

    case get_from_cache(cache_key) do
      {:ok, value} when not refresh ->
        value

      _ ->
        value =
          profile operation, metadata: %{cache_miss: true} do
            fun.()
          end

        put_in_cache(cache_key, value, ttl)
        value
    end
  end

  # Private functions

  defp poolboy_available? do
    Code.ensure_loaded?(:poolboy)
  end

  @spec init_poolboy_pool(String.t() | atom(), keyword()) :: any()
  defp init_poolboy_pool(pool_name, opts) do
    pool_config = [
      name: {:local, pool_name},
      worker_module:
        Keyword.get(opts, :worker, Raxol.Core.Performance.DefaultWorker),
      size: Keyword.get(opts, :size, 5),
      max_overflow: Keyword.get(opts, :max_overflow, 10)
    ]

    worker_args = Keyword.get(opts, :worker_args, [])

    case :poolboy.start_link(pool_config, worker_args) do
      {:ok, _pid} -> {:ok, pool_name}
      {:error, reason} -> {:error, reason}
    end
  end

  @spec init_simple_pool(String.t() | atom(), keyword()) :: any()
  defp init_simple_pool(pool_name, _opts) do
    # Simple connection tracking using Registry
    registry_name = :"#{pool_name}_connections"

    case Registry.start_link(keys: :unique, name: registry_name) do
      {:ok, _pid} ->
        Log.info("Initialized simple connection pool: #{pool_name}")
        {:ok, pool_name}

      {:error, {:already_started, _pid}} ->
        {:ok, pool_name}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @spec get_or_create_connection(String.t() | atom()) :: any() | nil
  defp get_or_create_connection(pool_name) do
    registry_name = :"#{pool_name}_connections"
    connection_id = "conn_#{System.unique_integer([:positive])}"

    case Registry.register(registry_name, connection_id, %{
           created_at: DateTime.utc_now()
         }) do
      {:ok, _pid} ->
        # In a real implementation, this would create an actual connection
        # For now, return a mock connection identifier
        {:ok, %{id: connection_id, pool: pool_name}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @spec return_connection(String.t() | atom(), any()) :: any()
  defp return_connection(pool_name, conn) do
    registry_name = :"#{pool_name}_connections"
    Registry.unregister(registry_name, conn.id)
    :ok
  end

  @spec get_from_cache(any()) :: any() | nil
  defp get_from_cache(key) do
    case :persistent_term.get({:cache, key}, :not_found) do
      {:cached, value, expiry} ->
        case expiry > System.system_time(:millisecond) do
          true -> {:ok, value}
          false -> :miss
        end

      _ ->
        :miss
    end
  end

  @spec put_in_cache(any(), any(), any()) :: any()
  defp put_in_cache(key, value, ttl) do
    expiry = System.system_time(:millisecond) + ttl
    :persistent_term.put({:cache, key}, {:cached, value, expiry})
  end

  @spec lookup_cache(String.t() | atom(), any()) :: any()
  defp lookup_cache(_cache_name, key) do
    get_from_cache(key)
  end

  @spec store_cache(String.t() | atom(), any(), any(), any()) :: any()
  defp store_cache(_cache_name, key, value, ttl) do
    put_in_cache(key, value, ttl)
  end

  @spec length_or_size(any()) :: any()
  defp length_or_size(data) when is_list(data), do: length(data)
  @spec length_or_size(any()) :: any()
  defp length_or_size(data) when is_map(data), do: map_size(data)
  @spec length_or_size(any()) :: any()
  defp length_or_size(_), do: 0

  @spec calculate_improvement_potential(any()) :: any()
  defp calculate_improvement_potential(results) do
    times = Enum.map(results, fn {_, stats} -> stats.mean end)
    best = Enum.min(times)
    worst = Enum.max(times)

    %{
      absolute: worst - best,
      percentage: (worst - best) / worst * 100
    }
  end
end
