defmodule Towerops.Snmp.Profiles.Mikrotik do
  @moduledoc """
  MikroTik RouterOS device profile.
  Extends Base profile with MikroTik-specific monitoring via MIKROTIK-MIB.

  Discovers and monitors:
  - RouterBoard hardware information
  - Health sensors (temperature, voltage, current, fan speed)
  - CPU and memory usage
  - Power supply status
  - Wireless interfaces (if present)
  """

  alias Towerops.Snmp.Client
  alias Towerops.Snmp.Discovery
  alias Towerops.Snmp.Profiles.Base

  require Logger

  # MikroTik enterprise OID: 1.3.6.1.4.1.14988
  @mikrotik_oids %{
    # System Information (mtxrSystem)
    serial_number: "1.3.6.1.4.1.14988.1.1.7.3.0",
    firmware_version: "1.3.6.1.4.1.14988.1.1.7.4.0",
    board_name: "1.3.6.1.4.1.14988.1.1.7.9.0",
    display_name: "1.3.6.1.4.1.14988.1.1.7.8.0",
    build_time: "1.3.6.1.4.1.14988.1.1.7.6.0",
    license_version: "1.3.6.1.4.1.14988.1.1.4.4.0",

    # Health - Voltages (mtxrHealth)
    core_voltage: "1.3.6.1.4.1.14988.1.1.3.1.0",
    three_volt: "1.3.6.1.4.1.14988.1.1.3.2.0",
    five_volt: "1.3.6.1.4.1.14988.1.1.3.3.0",
    twelve_volt: "1.3.6.1.4.1.14988.1.1.3.4.0",
    voltage: "1.3.6.1.4.1.14988.1.1.3.8.0",

    # Health - Temperatures
    sensor_temperature: "1.3.6.1.4.1.14988.1.1.3.5.0",
    cpu_temperature: "1.3.6.1.4.1.14988.1.1.3.6.0",
    board_temperature: "1.3.6.1.4.1.14988.1.1.3.7.0",
    temperature: "1.3.6.1.4.1.14988.1.1.3.10.0",
    processor_temperature: "1.3.6.1.4.1.14988.1.1.3.11.0",

    # Health - Power & Current
    current: "1.3.6.1.4.1.14988.1.1.3.13.0",
    power: "1.3.6.1.4.1.14988.1.1.3.12.0",

    # Health - Fans & Cooling
    active_fan: "1.3.6.1.4.1.14988.1.1.3.9.0",
    fan_speed_1: "1.3.6.1.4.1.14988.1.1.3.17.0",
    fan_speed_2: "1.3.6.1.4.1.14988.1.1.3.18.0",

    # Health - Power Supply
    processor_frequency: "1.3.6.1.4.1.14988.1.1.3.14.0",
    power_supply_state: "1.3.6.1.4.1.14988.1.1.3.15.0",
    backup_psu_state: "1.3.6.1.4.1.14988.1.1.3.16.0",

    # System Resources
    cpu_load: "1.3.6.1.2.1.25.3.3.1.2.1",
    total_memory: "1.3.6.1.2.1.25.2.2.0",
    used_memory: "1.3.6.1.4.1.14988.1.1.1.2.0",
    total_hdd: "1.3.6.1.4.1.14988.1.1.1.6.0",
    used_hdd: "1.3.6.1.4.1.14988.1.1.1.7.0"
  }

  # Table OID bases for walking
  @poe_table "1.3.6.1.4.1.14988.1.1.15.1"
  @optical_table "1.3.6.1.4.1.14988.1.1.16.1"

  @doc """
  Discovers system information using Base profile and adds MikroTik-specific details.
  """
  @spec discover_system_info(Client.connection_opts()) ::
          {:ok, Discovery.system_info()} | {:error, term()}
  def discover_system_info(client_opts) do
    with {:ok, base_info} <- Base.discover_system_info(client_opts),
         {:ok, mikrotik_info} <- discover_mikrotik_system_info(client_opts) do
      {:ok, Map.merge(base_info, mikrotik_info)}
    end
  end

  @doc """
  Discovers network interfaces using Base profile.
  """
  @spec discover_interfaces(Client.connection_opts()) ::
          {:ok, [Discovery.interface_data()]} | {:error, term()}
  def discover_interfaces(client_opts) do
    Base.discover_interfaces(client_opts)
  end

  @doc """
  Discovers MikroTik health sensors and system resources.
  Returns comprehensive sensor list including temperature, voltage, current, fans, CPU, memory, disk.
  Also discovers entity sensors from ENTITY-SENSOR-MIB (for additional temperature, voltage, etc.).
  """
  @spec discover_sensors(Client.connection_opts()) ::
          {:ok, [Discovery.sensor_data()]} | {:error, term()}
  def discover_sensors(client_opts) do
    Logger.debug("Discovering MikroTik sensors...")

    health_sensors = discover_health_sensors(client_opts)
    resource_sensors = discover_resource_sensors(client_opts)

    # Also try to discover entity sensors (for additional temperature, voltage sensors)
    {:ok, entity_sensors} = Base.discover_sensors(client_opts)

    all_sensors = health_sensors ++ resource_sensors ++ entity_sensors

    if all_sensors == [] do
      Logger.warning("No MikroTik sensors found")
      {:ok, []}
    else
      Logger.debug("Discovered #{length(all_sensors)} MikroTik sensors")
      {:ok, all_sensors}
    end
  end

  @doc """
  Identifies MikroTik device from sysDescr.
  """
  @spec identify_device(Discovery.system_info()) :: Discovery.device_info()
  def identify_device(system_info) do
    sys_descr = Map.get(system_info, :sys_descr, "")

    # Extract model from sysDescr (e.g., "RouterOS RB5009UG+S+")
    model =
      case Regex.run(~r/RouterOS\s+([A-Z0-9\+\-]+)/, sys_descr) do
        [_, model] -> model
        _ -> "RouterOS Device"
      end

    firmware = Map.get(system_info, :firmware_version) || extract_firmware_from_sysdescr(sys_descr)

    Map.merge(system_info, %{
      manufacturer: "MikroTik",
      model: model,
      firmware_version: firmware
    })
  end

  # Private functions

  defp discover_mikrotik_system_info(client_opts) do
    oids = [
      @mikrotik_oids.serial_number,
      @mikrotik_oids.firmware_version,
      @mikrotik_oids.board_name,
      @mikrotik_oids.display_name,
      @mikrotik_oids.license_version
    ]

    case Client.get_multiple(client_opts, oids) do
      {:ok, [serial, firmware, board, display, license]} ->
        {:ok,
         %{
           serial_number: serial,
           firmware_version: firmware,
           board_name: board,
           display_name: display,
           license_version: license
         }}

      {:error, _} ->
        # MikroTik-specific OIDs not available or partially available
        {:ok, %{}}
    end
  end

  defp discover_health_sensors(client_opts) do
    # Try to get all health OIDs - voltages, temperatures, power, fans
    health_oids = [
      # Voltages
      {@mikrotik_oids.core_voltage, "core_voltage", "V", 10},
      {@mikrotik_oids.three_volt, "3v3_voltage", "V", 10},
      {@mikrotik_oids.five_volt, "5v_voltage", "V", 10},
      {@mikrotik_oids.twelve_volt, "12v_voltage", "V", 10},
      {@mikrotik_oids.voltage, "input_voltage", "V", 10},
      # Temperatures
      {@mikrotik_oids.sensor_temperature, "sensor_temperature", "°C", 10},
      {@mikrotik_oids.cpu_temperature, "cpu_temperature", "°C", 10},
      {@mikrotik_oids.board_temperature, "board_temperature", "°C", 10},
      {@mikrotik_oids.temperature, "temperature", "°C", 10},
      {@mikrotik_oids.processor_temperature, "processor_temperature", "°C", 10},
      # Power & Current
      {@mikrotik_oids.current, "current", "mA", 1},
      {@mikrotik_oids.power, "power", "W", 10},
      # Fans
      {@mikrotik_oids.fan_speed_1, "fan1", "RPM", 1},
      {@mikrotik_oids.fan_speed_2, "fan2", "RPM", 1},
      # Processor
      {@mikrotik_oids.processor_frequency, "processor_frequency", "MHz", 1}
    ]

    health_oids
    |> Enum.map(fn {oid, type, unit, divisor} ->
      case Client.get(client_opts, oid) do
        {:ok, value} when is_integer(value) and value > 0 ->
          %{
            sensor_type: type,
            sensor_index: type,
            sensor_oid: oid,
            sensor_descr: format_sensor_name(type),
            sensor_unit: unit,
            sensor_divisor: divisor,
            last_value: value / divisor,
            status: "ok"
          }

        _ ->
          nil
      end
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp discover_resource_sensors(client_opts) do
    cpu_sensors = discover_cpu_sensors(client_opts)
    storage_sensors = discover_storage_sensors(client_opts)
    poe_sensors = discover_poe_sensors(client_opts)
    optical_sensors = discover_optical_sensors(client_opts)

    cpu_sensors ++ storage_sensors ++ poe_sensors ++ optical_sensors
  end

  defp discover_cpu_sensors(client_opts) do
    # Walk hrProcessorLoad (1.3.6.1.2.1.25.3.3.1.2) to get CPU load per core
    case Client.walk(client_opts, "1.3.6.1.2.1.25.3.3.1.2") do
      {:ok, results} ->
        Enum.map(results, fn {oid, load} ->
          index = oid |> String.split(".") |> List.last()

          %{
            sensor_type: "cpu_load",
            sensor_index: "cpu#{index}",
            sensor_oid: oid,
            sensor_descr: "CPU #{index} Load",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: load / 1.0,
            status: cpu_status(load)
          }
        end)

      _ ->
        []
    end
  end

  defp discover_storage_sensors(client_opts) do
    with {:ok, descr_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.3"),
         {:ok, size_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.5"),
         {:ok, used_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.6") do
      indices = extract_storage_indices(descr_results)
      build_storage_sensors(indices, descr_results, size_results, used_results)
    else
      _ -> []
    end
  end

  defp extract_storage_indices(descr_results) do
    descr_results
    |> Map.keys()
    |> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
  end

  defp build_storage_sensors(indices, descr_results, size_results, used_results) do
    indices
    |> Enum.map(&build_storage_sensor(&1, descr_results, size_results, used_results))
    |> Enum.reject(&is_nil/1)
  end

  defp build_storage_sensor(index, descr_results, size_results, used_results) do
    descr_oid = "1.3.6.1.2.1.25.2.3.1.3.#{index}"
    size_oid = "1.3.6.1.2.1.25.2.3.1.5.#{index}"
    used_oid = "1.3.6.1.2.1.25.2.3.1.6.#{index}"

    descr = Map.get(descr_results, descr_oid, "")
    size = Map.get(size_results, size_oid, 0)
    used = Map.get(used_results, used_oid, 0)

    if size > 0 do
      percent = used / size * 100
      type = determine_storage_type(descr)

      # Cap percentage at 100% and log warning if exceeded
      # This can happen if the device reports incorrect SNMP values
      capped_percent =
        if percent > 100 do
          Logger.warning(
            "Storage sensor #{descr} exceeded 100%: #{Float.round(percent, 2)}% " <>
              "(used=#{used} from #{used_oid}, size=#{size} from #{size_oid}). " <>
              "Capping at 100%. This may indicate a device firmware bug."
          )

          100.0
        else
          percent
        end

      %{
        sensor_type: "#{type}_usage",
        sensor_index: index,
        sensor_oid: used_oid,
        sensor_descr: descr,
        sensor_unit: "%",
        sensor_divisor: 1,
        last_value: capped_percent,
        status: storage_status(type, capped_percent),
        metadata: %{
          size_oid: size_oid,
          calculation: "percentage"
        }
      }
    end
  end

  defp determine_storage_type(descr) do
    cond do
      String.contains?(descr, "memory") -> "memory"
      String.contains?(descr, "disk") -> "disk"
      true -> "storage"
    end
  end

  defp discover_poe_sensors(client_opts) do
    # Walk POE table to get per-port POE statistics
    # Table columns: Name(3), Status(4), Voltage(5), Current(6), Power(7)
    with {:ok, name_results} <- Client.walk(client_opts, "#{@poe_table}.1.3"),
         {:ok, voltage_results} <- Client.walk(client_opts, "#{@poe_table}.1.5"),
         {:ok, current_results} <- Client.walk(client_opts, "#{@poe_table}.1.6"),
         {:ok, power_results} <- Client.walk(client_opts, "#{@poe_table}.1.7") do
      # Get indices from the name results
      indices =
        name_results
        |> Map.keys()
        |> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)

      # Create sensors for voltage, current, and power per port
      Enum.flat_map(indices, fn index ->
        name_oid = "#{@poe_table}.1.3.#{index}"
        voltage_oid = "#{@poe_table}.1.5.#{index}"
        current_oid = "#{@poe_table}.1.6.#{index}"
        power_oid = "#{@poe_table}.1.7.#{index}"

        port_name = Map.get(name_results, name_oid, "Port #{index}")
        voltage = Map.get(voltage_results, voltage_oid, 0)
        current = Map.get(current_results, current_oid, 0)
        power = Map.get(power_results, power_oid, 0)

        Enum.reject(
          [
            if voltage > 0 do
              %{
                sensor_type: "poe_voltage",
                sensor_index: "poe_#{index}_voltage",
                sensor_oid: voltage_oid,
                sensor_descr: "#{port_name} POE Voltage",
                sensor_unit: "V",
                sensor_divisor: 10,
                last_value: voltage / 10.0,
                status: "ok"
              }
            end,
            if current > 0 do
              %{
                sensor_type: "poe_current",
                sensor_index: "poe_#{index}_current",
                sensor_oid: current_oid,
                sensor_descr: "#{port_name} POE Current",
                sensor_unit: "mA",
                sensor_divisor: 1,
                last_value: current / 1.0,
                status: "ok"
              }
            end,
            if power > 0 do
              %{
                sensor_type: "poe_power",
                sensor_index: "poe_#{index}_power",
                sensor_oid: power_oid,
                sensor_descr: "#{port_name} POE Power",
                sensor_unit: "W",
                sensor_divisor: 10,
                last_value: power / 10.0,
                status: "ok"
              }
            end
          ],
          &is_nil/1
        )
      end)
    else
      _ -> []
    end
  end

  defp discover_optical_sensors(client_opts) do
    # Walk optical table to get SFP/optical monitoring data
    # Table columns: Name(2), Temperature(3), Voltage(5), TxBiasCurrent(6), TxPower(7), RxPower(8)
    with {:ok, name_results} <- Client.walk(client_opts, "#{@optical_table}.1.2"),
         {:ok, temp_results} <- Client.walk(client_opts, "#{@optical_table}.1.3"),
         {:ok, voltage_results} <- Client.walk(client_opts, "#{@optical_table}.1.5"),
         {:ok, tx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.7"),
         {:ok, rx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.8") do
      # Get indices from the name results
      indices =
        name_results
        |> Map.keys()
        |> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)

      # Create sensors for each optical metric
      Enum.flat_map(indices, fn index ->
        name_oid = "#{@optical_table}.1.2.#{index}"
        temp_oid = "#{@optical_table}.1.3.#{index}"
        voltage_oid = "#{@optical_table}.1.5.#{index}"
        tx_power_oid = "#{@optical_table}.1.7.#{index}"
        rx_power_oid = "#{@optical_table}.1.8.#{index}"

        sfp_name = Map.get(name_results, name_oid, "SFP #{index}")
        temperature = Map.get(temp_results, temp_oid)
        voltage = Map.get(voltage_results, voltage_oid)
        tx_power = Map.get(tx_power_results, tx_power_oid)
        rx_power = Map.get(rx_power_results, rx_power_oid)

        Enum.reject(
          [
            if temperature do
              %{
                sensor_type: "sfp_temperature",
                sensor_index: "sfp_#{index}_temp",
                sensor_oid: temp_oid,
                sensor_descr: "#{sfp_name} Temperature",
                sensor_unit: "°C",
                sensor_divisor: 1000,
                last_value: temperature / 1000.0,
                status: "ok"
              }
            end,
            if voltage do
              %{
                sensor_type: "sfp_voltage",
                sensor_index: "sfp_#{index}_voltage",
                sensor_oid: voltage_oid,
                sensor_descr: "#{sfp_name} Voltage",
                sensor_unit: "V",
                sensor_divisor: 1000,
                last_value: voltage / 1000.0,
                status: "ok"
              }
            end,
            if tx_power do
              %{
                sensor_type: "sfp_tx_power",
                sensor_index: "sfp_#{index}_tx",
                sensor_oid: tx_power_oid,
                sensor_descr: "#{sfp_name} TX Power",
                sensor_unit: "dBm",
                sensor_divisor: 1000,
                last_value: tx_power / 1000.0,
                status: "ok"
              }
            end,
            if rx_power do
              %{
                sensor_type: "sfp_rx_power",
                sensor_index: "sfp_#{index}_rx",
                sensor_oid: rx_power_oid,
                sensor_descr: "#{sfp_name} RX Power",
                sensor_unit: "dBm",
                sensor_divisor: 1000,
                last_value: rx_power / 1000.0,
                status: "ok"
              }
            end
          ],
          &is_nil/1
        )
      end)
    else
      _ -> []
    end
  end

  defp format_sensor_name("core_voltage"), do: "Core Voltage"
  defp format_sensor_name("3v3_voltage"), do: "3.3V Supply"
  defp format_sensor_name("5v_voltage"), do: "5V Supply"
  defp format_sensor_name("12v_voltage"), do: "12V Supply"
  defp format_sensor_name("input_voltage"), do: "Input Voltage"
  defp format_sensor_name("sensor_temperature"), do: "Sensor Chip Temperature"
  defp format_sensor_name("cpu_temperature"), do: "CPU Temperature"
  defp format_sensor_name("board_temperature"), do: "Board Temperature"
  defp format_sensor_name("temperature"), do: "System Temperature"
  defp format_sensor_name("processor_temperature"), do: "Processor Temperature"
  defp format_sensor_name("current"), do: "Current Draw"
  defp format_sensor_name("power"), do: "Power Consumption"
  defp format_sensor_name("fan1"), do: "Fan 1 Speed"
  defp format_sensor_name("fan2"), do: "Fan 2 Speed"
  defp format_sensor_name("processor_frequency"), do: "Processor Frequency"
  defp format_sensor_name(other), do: String.capitalize(other)

  defp cpu_status(load) when load < 70, do: "ok"
  defp cpu_status(load) when load < 90, do: "warning"
  defp cpu_status(_), do: "critical"

  defp storage_status("memory", percent) when percent < 80, do: "ok"
  defp storage_status("memory", percent) when percent < 95, do: "warning"
  defp storage_status("memory", _), do: "critical"

  defp storage_status("disk", percent) when percent < 85, do: "ok"
  defp storage_status("disk", percent) when percent < 95, do: "warning"
  defp storage_status("disk", _), do: "critical"

  defp storage_status(_, percent) when percent < 85, do: "ok"
  defp storage_status(_, percent) when percent < 95, do: "warning"
  defp storage_status(_, _), do: "critical"

  defp extract_firmware_from_sysdescr(sys_descr) do
    # Try to extract version from sysDescr
    # Example: "RouterOS RB5009UG+S+ (stable) 7.13.2"
    case Regex.run(~r/RouterOS.*?(\d+\.\d+(?:\.\d+)?)/, sys_descr) do
      [_, version] -> version
      _ -> nil
    end
  end
end
