defmodule Towerops.Application do
  @moduledoc """
  OTP Application module for Towerops.

  Supervises the Repo, PubSub, Telemetry, Endpoint, and monitoring workers.
  """

  use Application

  alias Towerops.Workers.JobCleanupTask

  require Logger

  # Capture the build timestamp at compile time (fallback for development)
  @build_timestamp DateTime.utc_now()

  @impl true
  def start(_type, _args) do
    # Ensure parsetools is started for MIB parsing (yecc dependency)
    _ = Application.ensure_all_started(:parsetools)

    # Register file logger backend for development (using new LoggerBackends API)
    if Application.get_env(:towerops, :env) == :dev do
      register_file_logger()
    end

    # Add snmpkit ebin directory to code path for mib_grammar_elixir.beam
    # This is needed because snmpkit has vendored Erlang modules (compiled from .yrl)
    # Note: With pre-compiled MIBs, this may no longer be needed
    snmpkit_ebin = Application.app_dir(:towerops, "../snmpkit/ebin")

    _ =
      if File.dir?(snmpkit_ebin) do
        :code.add_patha(to_charlist(snmpkit_ebin))
      end

    # Run migrations on startup (Ecto handles locking for concurrent runs)
    _ =
      if Application.get_env(:towerops, Towerops.Repo)[:database] != "towerops_test" do
        Towerops.Release.migrate()
      end

    # Configure MIB directories for NIF resolution (before supervisor starts)
    Logger.info("Configuring MIB directories for NIF resolution...")
    mib_dir = Application.app_dir(:towerops, "priv/mibs")

    # Build list of all MIB directories (including subdirectories for vendor MIBs)
    mib_dirs =
      if File.dir?(mib_dir) do
        # Get main directory plus all subdirectories
        subdirs =
          mib_dir
          |> File.ls!()
          |> Enum.map(&Path.join(mib_dir, &1))
          |> Enum.filter(&File.dir?/1)

        [mib_dir | subdirs]
      else
        [mib_dir]
      end

    # Set MIBDIRS environment variable for net-snmp (must be set BEFORE init)
    # Join with colon for Unix-style path list
    mibdirs_env = Enum.join(mib_dirs, ":")
    System.put_env("MIBDIRS", mibdirs_env)
    System.put_env("MIBS", "ALL")

    Logger.info("MIB directories configured: #{length(mib_dirs)} directories")

    # Add each directory to net-snmp's search path
    Enum.each(mib_dirs, fn dir ->
      :ok = ToweropsNative.load_mib_directory(dir)
    end)

    # Initialize the MIB library (loads all MIBs into memory)
    case ToweropsNative.init_mib_library() do
      result when result in ["initialized", "already_initialized"] ->
        Logger.info("MIB library initialized: #{result}")

      other ->
        Logger.warning("Unexpected MIB library init result: #{inspect(other)}")
    end

    topologies = Application.get_env(:libcluster, :topologies, [])

    # Supervision tree order matters for shutdown: children shut down in reverse start order.
    # Oban is placed AFTER its dependencies (SnmpKit, TaskSupervisor) so it shuts down
    # BEFORE them, allowing in-flight jobs to complete cleanly during the grace period.
    children =
      [
        {Cluster.Supervisor, [topologies, [name: Towerops.ClusterSupervisor]]},
        ToweropsWeb.Telemetry,
        Towerops.Repo,
        # Encryption vault for sensitive data (passwords, API tokens)
        Towerops.Vault,
        # Redis connection for brute force protection and rate limiting
        redis_spec(),
        # Rate limiting backend (ETS-based, per-node)
        {Towerops.RateLimit, [clean_period: to_timeout(minute: 1)]},
        {DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
        pubsub_spec(),
        # MIB name cache for fast profile matching (starts asynchronous pre-resolution)
        Towerops.Profiles.MibCache,
        # Load YAML profiles into ETS cache (depends on MibCache)
        Towerops.Profiles.YamlProfiles,
        # SnmpKit services for SNMP operations (used for SNMP protocol, not MIB resolution)
        SnmpKit.SnmpMgr.Config,
        SnmpKit.SnmpMgr.MIB,
        # Task supervisor for fire-and-forget background tasks (e.g. polling data processing)
        {Task.Supervisor, name: Towerops.TaskSupervisor},
        # Oban after its dependencies so it drains jobs before they shut down
        {Oban, Application.fetch_env!(:towerops, Oban)}
      ] ++
        dev_only_workers() ++
        background_workers() ++
        [
          # Start to serve requests, typically the last entry
          ToweropsWeb.Endpoint
        ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Towerops.Supervisor]
    result = Supervisor.start_link(children, opts)

    # Run post-startup cleanup tasks (production only)
    # This ensures all polling jobs use the latest worker code after deployment
    Task.start(fn ->
      # Wait for supervisor to fully initialize
      Process.sleep(2000)
      JobCleanupTask.run()
    end)

    result
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  @impl true
  def config_change(changed, _new, removed) do
    ToweropsWeb.Endpoint.config_change(changed, removed)
    :ok
  end

  # Configure background workers - don't start in test environment
  defp background_workers do
    if Application.get_env(:towerops, :env) == :test do
      []
    else
      [
        # Start event logger (subscribes to PubSub)
        Towerops.Devices.EventLogger
        # Note: NeighborCleanupWorker, StaleAgentWorker, AgentLatencyEvaluator,
        # and JobHealthCheckWorker are now Oban Cron jobs (see config/runtime.exs)
      ]
    end
  end

  # Configure dev-only workers - profile watcher for auto-reload
  defp dev_only_workers do
    if Application.get_env(:towerops, :env) == :dev do
      [
        # Watch YAML profiles for changes and auto-reload
        Towerops.Profiles.ProfileWatcher
      ]
    else
      []
    end
  end

  # Returns Redis connection spec for brute force protection
  defp redis_spec do
    redis_config = Application.get_env(:towerops, :redis, [])

    if Keyword.has_key?(redis_config, :host) do
      opts = [
        host: Keyword.get(redis_config, :host, "localhost"),
        port: Keyword.get(redis_config, :port, 6379),
        name: Towerops.Redix
      ]

      # Add password if configured
      opts =
        case Keyword.get(redis_config, :password) do
          nil -> opts
          "" -> opts
          password -> Keyword.put(opts, :password, password)
        end

      {Redix, opts}
    else
      # No Redis configured - start a stub process that will fail gracefully
      # This allows the app to start in environments without Redis
      Supervisor.child_spec({Agent, fn -> nil end}, id: Towerops.Redix)
    end
  end

  # Returns PubSub spec - uses Redis in production/clustered environments,
  # falls back to default PG2 adapter for Mix tasks and development
  defp pubsub_spec do
    redis_config = Application.get_env(:towerops, :redis, [])
    node_name = node()

    # Use Redis adapter only when:
    # 1. Redis config exists with host
    # 2. Node has a name (not nonode@nohost)
    use_redis? =
      Keyword.has_key?(redis_config, :host) &&
        node_name != :nonode@nohost

    if use_redis? do
      base_config = [name: Towerops.PubSub, adapter: Phoenix.PubSub.Redis]
      redis_opts = redis_pubsub_config()
      {Phoenix.PubSub, Keyword.merge(base_config, redis_opts)}
    else
      # Default PG2 adapter for Mix tasks and development
      {Phoenix.PubSub, name: Towerops.PubSub}
    end
  end

  defp redis_pubsub_config do
    redis_config = Application.get_env(:towerops, :redis, [])

    # Base configuration for Phoenix.PubSub.Redis
    base_opts = [
      host: Keyword.get(redis_config, :host, "localhost"),
      port: Keyword.get(redis_config, :port, 6379),
      node_name: node()
    ]

    # Add password if configured (read from application config set by runtime.exs)
    base_opts =
      case Keyword.get(redis_config, :password) do
        nil -> base_opts
        "" -> base_opts
        password -> Keyword.put(base_opts, :password, password)
      end

    # Add resilience options for better reconnection handling
    resilience_opts = [
      # Redix connection options for better resilience
      socket_opts: [
        # TCP keepalive to detect dead connections
        keepalive: true
      ],
      # Connection timeout (5 seconds)
      timeout: 5_000,
      # Backoff settings for reconnection
      backoff_initial: 500,
      backoff_max: 30_000,
      # Don't exit process on disconnection - let PubSub handle it
      exit_on_disconnection: false,
      # Sync connect to fail fast if Redis unavailable at startup
      sync_connect: false
    ]

    Keyword.merge(base_opts, resilience_opts)
  end

  # Register file logger backend for development (only available in :dev environment)
  defp register_file_logger do
    if Code.ensure_loaded?(LoggerBackends) and Code.ensure_loaded?(LoggerFileBackend) do
      # Add the backend with a unique identifier (using apply to avoid compile-time warnings)
      apply(LoggerBackends, :add, [{LoggerFileBackend, :file_log}])

      # Configure the backend with path and level
      Logger.configure_backend({LoggerFileBackend, :file_log},
        path: "_build/dev.log",
        level: :debug
      )
    else
      Logger.debug("LoggerBackends not available - file logging disabled (dev-only feature)")
    end
  end

  @doc """
  Returns the deployment timestamp.

  In production (Kubernetes), this reads from the DEPLOY_TIMESTAMP environment
  variable which is automatically set by GitLab CI during deployment. Falls back
  to compile time in development.

  The timestamp is set by GitLab CI using:
  ```bash
  kubectl set env deployment/towerops DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -n towerops
  ```

  This ensures all pods in the deployment show the same deployment time, regardless
  of when individual pods were created or restarted.
  """
  @spec build_timestamp() :: DateTime.t()
  def build_timestamp do
    case System.get_env("DEPLOY_TIMESTAMP") do
      nil ->
        # Development/fallback: use compile time
        @build_timestamp

      timestamp_string ->
        # Production: parse from environment variable
        case DateTime.from_iso8601(timestamp_string) do
          {:ok, datetime, _offset} -> datetime
          {:error, _} -> @build_timestamp
        end
    end
  end
end
