defmodule Mix.Tasks.ImportProfiles do
  @shortdoc "Import device profiles from LibreNMS YAML definitions"

  @moduledoc """
  Imports device profiles from YAML definition files.

  ## Usage

      # Import all profiles from priv/profiles (default)
      mix import_profiles

      # Import from a custom path
      mix import_profiles --source-path ~/librenms/resources/definitions

      # Import specific profiles
      mix import_profiles --profiles mikrotik,cisco,epmp

  ## Options

    * `--source-path` - Path to definitions directory (default: priv/profiles)
    * `--profiles` - Comma-separated list of profiles to import (default: all)

  ## Notes

    * Reads YAML files from os_detection/ and os_discovery/ subdirectories
    * Creates database profiles with MIB symbolic names
    * Updates existing profiles if they already exist
    * Run ./scripts/sync-librenms.sh to sync profiles from LibreNMS
  """

  use Mix.Task

  import Ecto.Query

  alias Towerops.Profiles
  alias Towerops.Profiles.DeviceOid
  alias Towerops.Profiles.SensorOid
  alias Towerops.Repo

  require Logger

  @requirements ["app.start"]

  def run(args) do
    {opts, _} =
      OptionParser.parse!(args,
        strict: [
          source_path: :string,
          profiles: :string
        ]
      )

    source_path = opts[:source_path] || default_source_path()
    profiles_filter = parse_profiles(opts[:profiles])

    if !File.exists?(source_path) do
      raise "Source directory not found: #{source_path}"
    end

    case profiles_filter do
      :all ->
        import_all_profiles(source_path)

      profile_list ->
        import_specific_profiles(source_path, profile_list)
    end
  end

  defp default_source_path do
    Path.join(:code.priv_dir(:towerops), "profiles")
  end

  defp parse_profiles(nil), do: :all
  defp parse_profiles(""), do: :all

  defp parse_profiles(profiles_string) do
    profiles_string
    |> String.split(",")
    |> Enum.map(&String.trim/1)
    |> Enum.reject(&(&1 == ""))
  end

  defp import_all_profiles(source_path) do
    Mix.shell().info("Discovering YAML files in #{source_path}...")

    detection_path = Path.join(source_path, "os_detection")

    yaml_files =
      detection_path
      |> Path.join("*.yaml")
      |> Path.wildcard()
      |> Enum.sort()

    Mix.shell().info("Found #{length(yaml_files)} YAML files")

    profile_names = Enum.map(yaml_files, &Path.basename(&1, ".yaml"))
    import_profiles_by_name(source_path, profile_names)
  end

  defp import_specific_profiles(source_path, profiles) do
    Mix.shell().info("Importing #{length(profiles)} specified profiles")
    import_profiles_by_name(source_path, profiles)
  end

  defp import_profiles_by_name(source_path, profile_names) do
    results =
      Enum.map(profile_names, fn profile_name ->
        Mix.shell().info("Importing profile: #{profile_name}")

        case import_profile(source_path, profile_name) do
          {:ok, _profile} ->
            Mix.shell().info("✓ #{profile_name}: created/updated successfully")
            {:ok, profile_name}

          {:error, reason} ->
            Mix.shell().error("✗ #{profile_name}: #{inspect(reason)}")
            {:error, profile_name, reason}
        end
      end)

    successes = Enum.count(results, &match?({:ok, _}, &1))
    failures = Enum.count(results, &match?({:error, _, _}, &1))

    Mix.shell().info("Import complete: #{successes} succeeded, #{failures} failed")
  end

  defp import_profile(source_path, profile_name) do
    detection_file = Path.join([source_path, "os_detection", "#{profile_name}.yaml"])
    discovery_file = Path.join([source_path, "os_discovery", "#{profile_name}.yaml"])

    with {:ok, detection_data} <- read_yaml_file(detection_file),
         # Discovery file is optional - use empty map if not found
         discovery_data = read_yaml_file_optional(discovery_file),
         {:ok, _profile} <- create_or_update_profile(detection_data, discovery_data, profile_name),
         # Reload profile to get correct binary ID
         profile when not is_nil(profile) <- Profiles.get_profile(profile_name),
         :ok <- import_device_oids(profile, discovery_data),
         :ok <- import_sensor_oids(profile, discovery_data) do
      {:ok, profile}
    end
  end

  defp read_yaml_file_optional(file_path) do
    case read_yaml_file(file_path) do
      {:ok, data} -> data
      {:error, _} -> %{}
    end
  end

  defp read_yaml_file(file_path) do
    with {:ok, yaml_content} <- File.read(file_path),
         {:ok, yaml_data} <- YamlElixir.read_from_string(yaml_content) do
      # YamlElixir can return either a map or a list with one map
      yaml_data =
        case yaml_data do
          [data] -> data
          data when is_map(data) -> data
          _ -> yaml_data
        end

      {:ok, yaml_data}
    else
      {:error, :enoent} ->
        {:error, "File not found: #{file_path}"}

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

  defp create_or_update_profile(detection_data, _discovery_data, profile_name) do
    # Extract detection patterns from os_detection file
    detection_pattern = extract_detection_pattern(detection_data)
    detection_oid = extract_detection_oid(detection_data)
    vendor = Map.get(detection_data, "text") || String.capitalize(profile_name)

    attrs = %{
      name: profile_name,
      vendor: vendor,
      detection_pattern: detection_pattern,
      detection_oid: detection_oid,
      priority: Map.get(detection_data, "priority", 100),
      enabled: true
    }

    # Check if profile exists
    case Profiles.get_profile(profile_name) do
      nil ->
        Profiles.create_profile(attrs)

      existing_profile ->
        Profiles.update_profile(existing_profile, attrs)
    end
  end

  defp extract_detection_pattern(yaml_data) do
    # Look for sysDescr patterns in discovery section
    case get_in(yaml_data, ["discovery"]) do
      discovery when is_list(discovery) ->
        sys_descr_patterns = Enum.flat_map(discovery, &extract_sys_descr_patterns/1)

        List.first(sys_descr_patterns)

      _ ->
        nil
    end
  end

  defp extract_sys_descr_patterns(%{"sysDescr" => patterns}) when is_list(patterns), do: patterns
  defp extract_sys_descr_patterns(_), do: []

  defp extract_detection_oid(yaml_data) do
    # Look for sysObjectID patterns in discovery section
    case get_in(yaml_data, ["discovery"]) do
      discovery when is_list(discovery) ->
        oid_patterns = Enum.flat_map(discovery, &extract_sys_object_id_patterns/1)

        List.first(oid_patterns)

      _ ->
        nil
    end
  end

  defp extract_sys_object_id_patterns(%{"sysObjectID" => patterns}) when is_list(patterns), do: patterns

  defp extract_sys_object_id_patterns(_), do: []

  defp import_device_oids(profile, yaml_data) do
    # Look for device info in various places
    device_oids = extract_device_oids(yaml_data)

    # Delete existing device OIDs
    Repo.delete_all(
      from o in DeviceOid,
        where: o.profile_id == ^profile.id
    )

    # Insert new device OIDs
    Enum.each(device_oids, fn {field, mib_name} ->
      Profiles.add_device_oid(profile.id, field, mib_name)
    end)

    :ok
  end

  defp extract_device_oids(yaml_data) do
    # Extract from modules.os section in os_discovery YAML
    # Get the modules.os section
    os_module = get_in(yaml_data, ["modules", "os"]) || %{}

    # Map of YAML field names to our field names
    field_mappings = [
      {"serial", "serial_number"},
      {"version", "firmware_version"},
      {"hardware", "hardware"},
      {"lat", "latitude"},
      {"long", "longitude"},
      {"features", "features"},
      {"sysName", "sys_name_oid"}
    ]

    Enum.flat_map(field_mappings, fn {yaml_field, db_field} ->
      case Map.get(os_module, yaml_field) do
        nil -> []
        # Handle list of OIDs (fallback order)
        mib_names when is_list(mib_names) -> [{db_field, List.first(mib_names)}]
        mib_name when is_binary(mib_name) -> [{db_field, mib_name}]
        _ -> []
      end
    end)
  end

  defp import_sensor_oids(profile, yaml_data) do
    # Look for sensor definitions
    sensors = extract_sensor_oids(yaml_data)

    # Delete existing sensor OIDs
    Repo.delete_all(
      from o in SensorOid,
        where: o.profile_id == ^profile.id
    )

    # Insert new sensor OIDs
    Enum.each(sensors, fn sensor_attrs ->
      Profiles.add_sensor_oid(profile.id, sensor_attrs)
    end)

    :ok
  end

  defp extract_sensor_oids(yaml_data) do
    # Extract from modules.sensors section in os_discovery YAML
    sensors_module = get_in(yaml_data, ["modules", "sensors"]) || %{}

    # Sensor types to extract
    sensor_types = ["temperature", "voltage", "current", "power", "fanspeed"]

    Enum.flat_map(sensor_types, fn sensor_type ->
      case get_in(sensors_module, [sensor_type, "data"]) do
        sensor_data when is_list(sensor_data) ->
          sensor_data
          |> Enum.map(&parse_sensor(&1, sensor_type))
          |> Enum.reject(&is_nil/1)

        _ ->
          []
      end
    end)
  end

  defp parse_sensor(sensor_def, sensor_type) do
    mib_name = Map.get(sensor_def, "oid") || Map.get(sensor_def, "value")
    num_oid = Map.get(sensor_def, "num_oid")

    cond do
      table_sensor?(num_oid) -> nil
      scalar_sensor?(mib_name) -> build_sensor_map(sensor_def, sensor_type, mib_name)
      true -> nil
    end
  end

  defp table_sensor?(num_oid) when is_binary(num_oid) do
    String.contains?(num_oid, "{{ $index }}")
  end

  defp table_sensor?(_), do: false

  defp scalar_sensor?(mib_name) when is_binary(mib_name) do
    String.ends_with?(mib_name, ".0")
  end

  defp scalar_sensor?(_), do: false

  defp build_sensor_map(sensor_def, sensor_type, mib_name) do
    %{
      sensor_type: sensor_type,
      mib_name: mib_name,
      sensor_descr: extract_sensor_description(sensor_def, sensor_type),
      sensor_unit: Map.get(sensor_def, "unit"),
      sensor_divisor: Map.get(sensor_def, "divisor", 1)
    }
  end

  defp extract_sensor_description(sensor_def, default_type) do
    case Map.get(sensor_def, "descr") do
      nil ->
        default_type

      descr when is_binary(descr) ->
        clean_description_template(descr, default_type)

      _ ->
        default_type
    end
  end

  defp clean_description_template(descr, default_type) do
    descr
    |> String.replace(~r/\{\{ [^}]+ \}\}/, "")
    |> String.trim()
    |> case do
      "" -> default_type
      cleaned -> cleaned
    end
  end
end
