defmodule SnmpKit.SnmpLib.Config do
  @moduledoc """
  Configuration management system for production SNMP deployments.

  This module provides a flexible, environment-aware configuration system designed
  for real-world SNMP management applications. Based on patterns proven in large-scale
  deployments like the DDumb project managing 1000+ devices.

  ## Features

  - **Environment-Aware**: Automatic detection of dev/test/prod environments
  - **Layered Configuration**: Application, environment, and runtime overrides
  - **Dynamic Updates**: Hot-reload configuration without service restart
  - **Validation**: Schema validation for all configuration values
  - **Secrets Management**: Secure handling of sensitive configuration data
  - **Multi-Tenant Support**: Per-deployment configuration isolation

  ## Configuration Sources (in order of precedence)

  1. Runtime environment variables
  2. Configuration files (config/*.exs)
  3. Application defaults
  4. Module defaults

  ## Usage Patterns

      # Get configuration for a specific component
      pool_config = SnmpKit.SnmpLib.Config.get(:pool, :default_settings)

      # Get configuration with fallback
      timeout = SnmpKit.SnmpLib.Config.get(:snmp, :timeout, 5000)

      # Update configuration at runtime
      SnmpKit.SnmpLib.Config.put(:pool, :max_size, 50)

      # Load configuration from file
      SnmpKit.SnmpLib.Config.load_from_file("/etc/snmp_lib/production.exs")

      # Validate current configuration
      {:ok, _} = SnmpKit.SnmpLib.Config.validate()

  ## Environment Detection

  The configuration system automatically detects the current environment:
  - `:dev` - Development with verbose logging and relaxed timeouts
  - `:test` - Testing with simulated backends and fast timeouts
  - `:prod` - Production with optimized settings and monitoring
  - `:staging` - Pre-production environment for integration testing

  ## Configuration Schema

  All configuration follows a validated schema to prevent runtime errors:

      %{
        snmp: %{
          default_version: :v2c,
          default_timeout: 5_000,
          default_retries: 3,
          default_community: "public"
        },
        pool: %{
          default_size: 10,
          max_overflow: 5,
          strategy: :fifo,
          health_check_interval: 30_000
        },
        monitoring: %{
          metrics_enabled: true,
          prometheus_port: 9090,
          dashboard_enabled: true,
          alert_thresholds: %{...}
        }
      }
  """

  use GenServer

  require Logger

  @config_table :snmp_lib_config
  @schema_table :snmp_lib_schema
  @watchers_table :snmp_lib_watchers

  @default_config %{
    snmp: %{
      default_version: :v2c,
      default_timeout: 5_000,
      default_retries: 3,
      default_community: "public",
      default_port: 161,
      max_message_size: 65_507,
      socket_options: [],
      mib_paths: []
    },
    pool: %{
      default_size: 10,
      max_overflow: 5,
      strategy: :fifo,
      health_check_interval: 30_000,
      checkout_timeout: 5_000,
      max_idle_time: 300_000
    },
    monitoring: %{
      metrics_enabled: true,
      prometheus_enabled: false,
      prometheus_port: 9090,
      dashboard_enabled: false,
      log_level: :info,
      alert_thresholds: %{
        error_rate: 0.05,
        response_time_p95: 2_000,
        connection_pool_utilization: 0.8
      }
    },
    error_handling: %{
      max_retries: 3,
      retry_strategy: :exponential,
      circuit_breaker_enabled: true,
      circuit_breaker_threshold: 10,
      circuit_breaker_timeout: 60_000
    },
    cache: %{
      enabled: true,
      # 5 minutes
      default_ttl: 300_000,
      max_size: 10_000,
      cleanup_interval: 60_000
    }
  }

  @type config_key :: atom() | [atom()]
  @type config_value :: any()
  @type config_section :: atom()
  @type environment :: :dev | :test | :prod | :staging

  ## Public API

  @doc """
  Starts the configuration manager with initial configuration.

  ## Options

  - `config_file`: Path to configuration file to load on startup
  - `environment`: Override automatic environment detection
  - `validate_on_start`: Validate configuration on startup (default: true)

  ## Examples

      {:ok, _pid} = SnmpKit.SnmpLib.Config.start_link()
      {:ok, _pid} = SnmpKit.SnmpLib.Config.start_link(config_file: "/etc/snmp_lib/prod.exs")
  """
  @spec start_link(keyword()) :: {:ok, pid()} | {:error, any()}
  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Gets a configuration value by key path with optional default.

  ## Parameters

  - `section`: Configuration section (:snmp, :pool, :monitoring, etc.)
  - `key`: Specific configuration key or nested key path
  - `default`: Value to return if key is not found

  ## Examples

      # Get a simple value
      timeout = SnmpKit.SnmpLib.Config.get(:snmp, :default_timeout)

      # Get nested value
      threshold = SnmpKit.SnmpLib.Config.get(:monitoring, [:alert_thresholds, :error_rate])

      # Get with default
      community = SnmpKit.SnmpLib.Config.get(:snmp, :community, "public")
  """
  @spec get(config_section(), config_key(), config_value()) :: config_value()
  def get(section, key, default \\ nil) do
    case :ets.lookup(@config_table, section) do
      [{^section, config}] ->
        get_nested_value(config, key, default)

      [] ->
        get_default_value(section, key, default)
    end
  end

  @doc """
  Sets a configuration value at runtime.

  Changes are applied immediately and optionally persisted.

  ## Examples

      :ok = SnmpKit.SnmpLib.Config.put(:pool, :default_size, 20)
      :ok = SnmpKit.SnmpLib.Config.put(:monitoring, [:alert_thresholds, :error_rate], 0.10)
  """
  @spec put(config_section(), config_key(), config_value()) :: :ok | {:error, any()}
  def put(section, key, value) do
    GenServer.call(__MODULE__, {:put, section, key, value})
  end

  @doc """
  Loads configuration from a file and merges with current config.

  ## Examples

      :ok = SnmpKit.SnmpLib.Config.load_from_file("/etc/snmp_lib/production.exs")
  """
  @spec load_from_file(binary()) :: :ok | {:error, any()}
  def load_from_file(file_path) do
    GenServer.call(__MODULE__, {:load_file, file_path})
  end

  @doc """
  Gets the current environment.

  ## Examples

      env = SnmpKit.SnmpLib.Config.environment()  # :prod
  """
  @spec environment() :: environment()
  def environment do
    GenServer.call(__MODULE__, :get_environment)
  end

  @doc """
  Validates the current configuration against the schema.

  ## Returns

  - `{:ok, config}`: Configuration is valid
  - `{:error, errors}`: List of validation errors

  ## Examples

      case SnmpKit.SnmpLib.Config.validate() do
        {:ok, _config} -> Logger.info("Configuration is valid")
        {:error, validation_errors} -> Logger.error("Configuration errors: " <> inspect(validation_errors))
      end
  """
  @spec validate() :: {:ok, map()} | {:error, [any()]}
  def validate do
    GenServer.call(__MODULE__, :validate)
  end

  @doc """
  Registers a callback function to be called when configuration changes.

  ## Examples

      SnmpKit.SnmpLib.Config.watch(:pool, fn old_config, new_config ->
        Logger.info("Pool configuration changed")
        SnmpKit.SnmpLib.Pool.reload_config(new_config)
      end)
  """
  @spec watch(config_section(), function()) :: :ok
  def watch(section, callback) when is_function(callback, 2) do
    GenServer.call(__MODULE__, {:watch, section, callback})
  end

  @doc """
  Gets all configuration as a nested map.

  ## Examples

      config = SnmpKit.SnmpLib.Config.all()
      IO.inspect(config.snmp.default_timeout)
  """
  @spec all() :: map()
  def all do
    GenServer.call(__MODULE__, :get_all)
  end

  @doc """
  Reloads configuration from environment and files.

  ## Examples

      :ok = SnmpKit.SnmpLib.Config.reload()
  """
  @spec reload() :: :ok | {:error, any()}
  def reload do
    GenServer.call(__MODULE__, :reload)
  end

  @doc """
  Merges user-provided options with default SNMP configuration values.

  This function provides the default SNMP configuration options that are commonly
  used across SNMP operations, then merges them with user-provided options. User
  options take precedence over defaults.

  ## Default Values

  - `community`: "public"
  - `timeout`: 5000 (milliseconds)
  - `retries`: 3
  - `port`: 161
  - `version`: :v2c
  - `mib_paths`: []

  ## Parameters

  - `opts`: Keyword list of user-provided options that override defaults

  ## Returns

  Keyword list with defaults merged with user options, where user options take precedence.

  ## Examples

      iex> SnmpKit.SnmpLib.Config.merge_opts([])
      [community: "public", timeout: 5000, retries: 3, port: 161, version: :v2c, mib_paths: []]

      iex> result = SnmpKit.SnmpLib.Config.merge_opts([timeout: 10000])
      iex> result[:community]
      "public"
      iex> result[:timeout]
      10000
      iex> result[:retries]
      3

      iex> result = SnmpKit.SnmpLib.Config.merge_opts([community: "private", port: 162])
      iex> result[:community]
      "private"
      iex> result[:port]
      162
      iex> result[:timeout]
      5000
  """
  @spec merge_opts(keyword()) :: keyword()
  def merge_opts(opts) when is_list(opts) do
    # Get default SNMP values from the configuration system, with static fallbacks
    defaults = [
      community: safe_get(:snmp, :default_community, "public"),
      timeout: safe_get(:snmp, :default_timeout, 5000),
      retries: safe_get(:snmp, :default_retries, 3),
      port: safe_get(:snmp, :default_port, 161),
      version: safe_get(:snmp, :default_version, :v2c),
      mib_paths: safe_get(:snmp, :mib_paths, [])
    ]

    # Merge defaults with user options, user options take precedence
    Keyword.merge(defaults, opts)
  end

  ## GenServer Implementation

  @impl GenServer
  def init(opts) do
    # Create ETS tables for fast access
    _ = :ets.new(@config_table, [:named_table, :set, :public, read_concurrency: true])
    _ = :ets.new(@schema_table, [:named_table, :set, :public, read_concurrency: true])
    _ = :ets.new(@watchers_table, [:named_table, :bag, :public])

    # Detect environment
    environment = detect_environment(opts)

    # Load initial configuration
    initial_config = load_initial_config(environment, opts)

    # Store configuration in ETS
    store_config(initial_config)

    # Validate if requested
    if Keyword.get(opts, :validate_on_start, true) do
      case validate_config(initial_config) do
        {:ok, _} ->
          Logger.info("SnmpKit.SnmpLib.Config started with valid configuration")

        {:error, errors} ->
          Logger.warning("SnmpKit.SnmpLib.Config started with validation errors: #{inspect(errors)}")
      end
    end

    state = %{
      environment: environment,
      config_file: Keyword.get(opts, :config_file),
      watchers: %{}
    }

    {:ok, state}
  end

  @impl GenServer
  def handle_call({:put, section, key, value}, _from, state) do
    case update_config_value(section, key, value) do
      :ok ->
        notify_watchers(section)
        {:reply, :ok, state}

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

  @impl GenServer
  def handle_call({:load_file, file_path}, _from, state) do
    case load_config_file(file_path) do
      {:ok, file_config} ->
        merge_config(file_config)
        {:reply, :ok, state}

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

  @impl GenServer
  def handle_call(:get_environment, _from, state) do
    {:reply, state.environment, state}
  end

  @impl GenServer
  def handle_call(:validate, _from, state) do
    current_config = get_all_config()
    result = validate_config(current_config)
    {:reply, result, state}
  end

  @impl GenServer
  def handle_call({:watch, section, callback}, _from, state) do
    :ets.insert(@watchers_table, {section, callback})
    {:reply, :ok, state}
  end

  @impl GenServer
  def handle_call(:get_all, _from, state) do
    config = get_all_config()
    {:reply, config, state}
  end

  @impl GenServer
  def handle_call(:reload, _from, state) do
    case reload_configuration(state) do
      {:ok, new_state} -> {:reply, :ok, new_state}
      {:error, error} -> {:reply, {:error, error}, state}
    end
  end

  ## Private Implementation

  # Environment detection with multiple fallbacks
  defp detect_environment(opts) do
    cond do
      env = Keyword.get(opts, :environment) -> env
      env = System.get_env("MIX_ENV") -> String.to_atom(env)
      env = System.get_env("SNMP_LIB_ENV") -> String.to_atom(env)
      env = Application.get_env(:snmp_lib, :environment) -> env
      true -> :dev
    end
  end

  # Load configuration from multiple sources
  defp load_initial_config(environment, opts) do
    base_config = get_environment_defaults(environment)

    # Load from application config
    app_config = :snmp_lib |> Application.get_all_env() |> Map.new()
    config_with_app = deep_merge(base_config, app_config)

    # Load from file if specified
    config_with_file =
      case Keyword.get(opts, :config_file) do
        nil ->
          config_with_app

        file_path ->
          case load_config_file(file_path) do
            {:ok, file_config} -> deep_merge(config_with_app, file_config)
            {:error, _} -> config_with_app
          end
      end

    # Load from environment variables
    config_with_env = load_from_environment(config_with_file)

    config_with_env
  end

  # Get environment-specific defaults
  defp get_environment_defaults(:dev) do
    @default_config
    |> put_in([:monitoring, :log_level], :debug)
    |> put_in([:pool, :health_check_interval], 10_000)
  end

  defp get_environment_defaults(:test) do
    @default_config
    |> put_in([:snmp, :default_timeout], 1_000)
    |> put_in([:pool, :default_size], 2)
    |> put_in([:pool, :health_check_interval], 5_000)
    |> put_in([:monitoring, :metrics_enabled], false)
    |> put_in([:cache, :enabled], false)
  end

  defp get_environment_defaults(:staging) do
    @default_config
    |> put_in([:monitoring, :dashboard_enabled], true)
    |> put_in([:monitoring, :prometheus_enabled], true)
  end

  defp get_environment_defaults(:prod) do
    @default_config
    |> put_in([:pool, :default_size], 25)
    |> put_in([:pool, :max_overflow], 15)
    |> put_in([:monitoring, :metrics_enabled], true)
    |> put_in([:monitoring, :dashboard_enabled], true)
    |> put_in([:monitoring, :prometheus_enabled], true)
    |> put_in([:monitoring, :log_level], :warning)
  end

  defp get_environment_defaults(_), do: @default_config

  # Load configuration from environment variables
  defp load_from_environment(config) do
    # This would parse environment variables like:
    # SNMP_LIB_POOL_DEFAULT_SIZE=20
    # SNMP_LIB_SNMP_DEFAULT_TIMEOUT=10000
    # etc.

    env_config = %{}

    # For now, just return the config as-is
    # In a full implementation, this would parse all SNMP_LIB_* environment variables
    deep_merge(config, env_config)
  end

  # Load configuration from file
  defp load_config_file(file_path) do
    if File.exists?(file_path) do
      try do
        {config, _} = Code.eval_file(file_path)
        {:ok, config}
      rescue
        error ->
          Logger.error("Failed to load config file #{file_path}: #{inspect(error)}")
          {:error, error}
      end
    else
      {:error, :file_not_found}
    end
  end

  # Store configuration in ETS tables
  defp store_config(config) do
    Enum.each(config, fn {section, section_config} ->
      :ets.insert(@config_table, {section, section_config})
    end)
  end

  # Get nested configuration value
  defp get_nested_value(config, key, default) when is_atom(key) do
    Map.get(config, key, default)
  end

  defp get_nested_value(config, keys, default) when is_list(keys) do
    get_in(config, keys) || default
  end

  # Get default value from static configuration
  defp get_default_value(section, key, default) do
    case Map.get(@default_config, section) do
      nil -> default
      section_config -> get_nested_value(section_config, key, default)
    end
  end

  # Safe get that works even when GenServer is not running (for doctests)
  defp safe_get(section, key, default) do
    get(section, key, default)
  rescue
    ArgumentError ->
      # ETS table doesn't exist, fall back to static config
      get_default_value(section, key, default)
  end

  # Update configuration value in ETS
  defp update_config_value(section, key, value) do
    case :ets.lookup(@config_table, section) do
      [{^section, config}] ->
        case put_nested_value_safe(config, key, value) do
          {:ok, updated_config} ->
            :ets.insert(@config_table, {section, updated_config})
            :ok

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

      [] ->
        # Create new section
        case put_nested_value_safe(%{}, key, value) do
          {:ok, new_config} ->
            :ets.insert(@config_table, {section, new_config})
            :ok

          {:error, reason} ->
            {:error, reason}
        end
    end
  rescue
    ArgumentError ->
      {:error, :invalid_config_path}

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

  # Set nested configuration value
  defp put_nested_value_safe(config, key, value) when is_atom(key) do
    {:ok, Map.put(config, key, value)}
  end

  defp put_nested_value_safe(config, keys, value) when is_list(keys) do
    {:ok, put_in(config, keys, value)}
  rescue
    error ->
      {:error, {:unexpected_error, error}}
  end

  # Get all configuration as map
  defp get_all_config do
    @config_table
    |> :ets.tab2list()
    |> Map.new()
  end

  # Merge configuration maps deeply
  defp deep_merge(left, right) when is_map(left) and is_map(right) do
    Map.merge(left, right, fn _key, left_val, right_val ->
      deep_merge(left_val, right_val)
    end)
  end

  defp deep_merge(_left, right), do: right

  # Merge new configuration with existing
  defp merge_config(new_config) do
    existing_config = get_all_config()
    merged_config = deep_merge(existing_config, new_config)
    store_config(merged_config)
  end

  # Validate configuration against schema
  defp validate_config(config) do
    # Simple validation - in production this would use a proper schema library
    errors = []

    errors = validate_section(config, :snmp, errors)
    errors = validate_section(config, :pool, errors)
    errors = validate_section(config, :monitoring, errors)

    case errors do
      [] -> {:ok, config}
      _ -> {:error, errors}
    end
  end

  defp validate_section(config, section, errors) do
    case Map.get(config, section) do
      nil -> [{:missing_section, section} | errors]
      section_config when is_map(section_config) -> errors
      _ -> [{:invalid_section_type, section} | errors]
    end
  end

  # Notify watchers of configuration changes
  defp notify_watchers(section) do
    case :ets.lookup(@watchers_table, section) do
      [] ->
        :ok

      watchers ->
        old_config = :ets.lookup(@config_table, section)
        new_config = :ets.lookup(@config_table, section)

        Enum.each(watchers, fn {^section, callback} ->
          try do
            callback.(old_config, new_config)
          rescue
            error ->
              Logger.warning("Configuration watcher failed: #{inspect(error)}")
          end
        end)
    end
  end

  defp reload_configuration(state) do
    # Reload from environment and files
    new_config = load_initial_config(state.environment, config_file: state.config_file)
    store_config(new_config)

    # Notify all watchers
    Enum.each([:snmp, :pool, :monitoring, :error_handling, :cache], fn section ->
      notify_watchers(section)
    end)

    {:ok, state}
  rescue
    error ->
      {:error, error}
  end
end
