defmodule Towerops.DeviceProfiles.Importer do
  @moduledoc """
  Imports external device profile definitions from YAML files.

  Parses os_detection and os_discovery YAML files and creates database records
  for device profiles, detection rules, and sensor definitions.
  """

  alias Towerops.DeviceProfiles
  alias Towerops.Repo

  require Logger

  @doc """
  Imports a device profile from external YAML files.

  ## Parameters
    - detection_file: Path to os_detection YAML file
    - discovery_file: Path to os_discovery YAML file (optional)

  ## Returns
    - {:ok, profile} on success
    - {:error, reason} on failure
  """
  def import_profile(detection_file, discovery_file \\ nil) do
    Repo.transaction(fn ->
      with {:ok, detection_data} <- parse_yaml_file(detection_file),
           {:ok, profile} <- create_profile_from_detection(detection_data),
           {:ok, profile} <- maybe_import_discovery(profile, discovery_file) do
        profile
      else
        {:error, reason} ->
          Logger.error("Failed to import profile: #{inspect(reason)}")
          Repo.rollback(reason)
      end
    end)
  end

  @doc """
  Imports a device profile from parsed data structures (instead of files).

  ## Parameters
    - detection_data: Parsed YAML map from os_detection
    - discovery_data: Parsed YAML map from os_discovery (optional)

  ## Returns
    - {:ok, profile} on success
    - {:error, reason} on failure
  """
  def import_profile_from_data(detection_data, discovery_data \\ nil) when is_map(detection_data) do
    Repo.transaction(fn ->
      with {:ok, profile} <- create_profile_from_detection(detection_data),
           {:ok, profile} <- maybe_import_discovery_data(profile, discovery_data) do
        profile
      else
        {:error, reason} ->
          Logger.error("Failed to import profile: #{inspect(reason)}")
          Repo.rollback(reason)
      end
    end)
  end

  @doc """
  Imports all profiles from a directory.
  """
  def import_all_from_directory(detection_dir, discovery_dir) do
    detection_files = Path.wildcard(Path.join(detection_dir, "*.yaml"))

    results =
      Enum.map(detection_files, fn detection_file ->
        os_name = Path.basename(detection_file, ".yaml")
        discovery_file = Path.join(discovery_dir, "#{os_name}.yaml")

        discovery_file =
          if File.exists?(discovery_file), do: discovery_file

        case import_profile(detection_file, discovery_file) do
          {:ok, profile} ->
            Logger.info("Imported profile: #{profile.os}")
            {:ok, profile}

          {:error, reason} ->
            Logger.warning("Failed to import #{os_name}: #{inspect(reason)}")
            {:error, os_name, reason}
        end
      end)

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

    Logger.info("Imported #{length(successes)} profiles successfully")

    if failures != [] do
      Logger.warning("Failed to import #{length(failures)} profiles")
    end

    {:ok, %{success: length(successes), failed: length(failures)}}
  end

  # Private functions

  defp parse_yaml_file(file_path) do
    case YamlElixir.read_from_file(file_path) do
      {:ok, data} -> {:ok, data}
      {:error, reason} -> {:error, "YAML parse error: #{inspect(reason)}"}
    end
  end

  defp create_profile_from_detection(data) do
    attrs = %{
      os: data["os"],
      text: data["text"] || data["os"],
      type: data["type"],
      icon: data["icon"],
      group: data["group"],
      snmp_bulk: Map.get(data, "snmp_bulk", true),
      mib_dir: data["mib_dir"],
      enabled: true,
      priority: data["priority"] || 100
    }

    # Upsert profile - updates if exists, creates if new
    # Deletes all associated definitions before update for clean re-import
    case DeviceProfiles.upsert_profile(attrs) do
      {:ok, profile} ->
        # Create detection rules (associations were deleted in upsert)
        :ok = create_detection_rules(profile.id, data["discovery"] || [])
        {:ok, profile}

      {:error, _} = error ->
        error
    end
  end

  defp create_detection_rules(profile_id, rules) when is_list(rules) do
    Enum.each(rules, fn rule ->
      parse_detection_rule(profile_id, rule)
    end)

    :ok
  end

  defp parse_detection_rule(profile_id, rule) do
    # Handle sysObjectID rules
    if Map.has_key?(rule, "sysObjectID") do
      create_sys_object_id_rules(profile_id, rule["sysObjectID"])
    end

    # Handle sysDescr rules
    if Map.has_key?(rule, "sysDescr") do
      DeviceProfiles.create_detection_rule(%{
        device_profile_id: profile_id,
        rule_type: "sysDescr",
        value: rule["sysDescr"]
      })
    end

    # Handle sysDescr_regex rules
    if Map.has_key?(rule, "sysDescr_regex") do
      create_sys_descr_regex_rules(profile_id, rule["sysDescr_regex"])
    end

    {:ok, :processed}
  end

  defp create_sys_object_id_rules(profile_id, oids) when is_list(oids) do
    Enum.each(oids, fn oid ->
      DeviceProfiles.create_detection_rule(%{
        device_profile_id: profile_id,
        rule_type: "sysObjectID",
        pattern: oid
      })
    end)

    {:ok, :created}
  end

  defp create_sys_object_id_rules(profile_id, oid) when is_binary(oid) do
    DeviceProfiles.create_detection_rule(%{
      device_profile_id: profile_id,
      rule_type: "sysObjectID",
      pattern: oid
    })
  end

  defp create_sys_descr_regex_rules(profile_id, patterns) when is_list(patterns) do
    Enum.each(patterns, fn pattern ->
      # Remove leading/trailing slashes from regex pattern
      clean_pattern = String.trim(pattern, "/")

      DeviceProfiles.create_detection_rule(%{
        device_profile_id: profile_id,
        rule_type: "sysDescr_regex",
        pattern: clean_pattern
      })
    end)

    {:ok, :created}
  end

  defp create_sys_descr_regex_rules(profile_id, pattern) when is_binary(pattern) do
    clean_pattern = String.trim(pattern, "/")

    DeviceProfiles.create_detection_rule(%{
      device_profile_id: profile_id,
      rule_type: "sysDescr_regex",
      pattern: clean_pattern
    })
  end

  defp maybe_import_discovery(profile, nil), do: {:ok, profile}

  defp maybe_import_discovery(profile, discovery_file) do
    if File.exists?(discovery_file) do
      with {:ok, discovery_data} <- parse_yaml_file(discovery_file) do
        import_discovery_definitions(profile, discovery_data)
      end
    else
      {:ok, profile}
    end
  end

  defp maybe_import_discovery_data(profile, nil), do: {:ok, profile}

  defp maybe_import_discovery_data(profile, discovery_data) when is_map(discovery_data) do
    import_discovery_definitions(profile, discovery_data)
  end

  defp import_discovery_definitions(profile, data) do
    # Import MIB if specified (truncate to 255 chars if needed)
    if data["mib"] do
      mib =
        if String.length(data["mib"]) > 255 do
          String.slice(data["mib"], 0, 255)
        else
          data["mib"]
        end

      DeviceProfiles.update_profile(profile, %{mib: mib})
    end

    modules = data["modules"] || %{}

    # Import OS definitions
    if modules["os"] do
      import_os_definitions(profile.id, modules["os"])
    end

    # Import processor definitions
    if modules["processors"] && modules["processors"]["data"] do
      import_processor_definitions(profile.id, modules["processors"]["data"])
    end

    # Import memory pool definitions
    if modules["mempools"] && modules["mempools"]["data"] do
      import_mempool_definitions(profile.id, modules["mempools"]["data"])
    end

    # Import sensor definitions
    if modules["sensors"] do
      import_sensor_definitions(profile.id, modules["sensors"])
    end

    {:ok, profile}
  end

  defp import_os_definitions(profile_id, os_data) do
    Enum.each(os_data, fn {field, value} ->
      attrs = %{
        device_profile_id: profile_id,
        field: to_string(field),
        oid: if(is_binary(value), do: value)
      }

      DeviceProfiles.create_os_definition(attrs)
    end)
  end

  defp import_processor_definitions(profile_id, processors) do
    Enum.each(processors, fn proc ->
      attrs = %{
        device_profile_id: profile_id,
        oid: proc["oid"],
        num_oid: proc["num_oid"],
        index: to_string(proc["index"] || ""),
        descr: proc["descr"],
        precision: proc["precision"] || 1,
        type: proc["type"]
      }

      DeviceProfiles.create_processor_definition(attrs)
    end)
  end

  defp import_mempool_definitions(profile_id, mempools) do
    Enum.each(mempools, fn mempool ->
      attrs = %{
        device_profile_id: profile_id,
        index: to_string(mempool["index"] || ""),
        descr: mempool["descr"],
        total_oid: mempool["total"],
        used_oid: mempool["used"],
        free_oid: mempool["free"],
        percent_used_oid: mempool["percent_used"],
        precision: mempool["precision"] || 1,
        type: mempool["type"],
        class: mempool["class"]
      }

      DeviceProfiles.create_mempool_definition(attrs)
    end)
  end

  defp import_sensor_definitions(profile_id, sensors_data) do
    Enum.each(sensors_data, fn {sensor_class, sensor_config} ->
      if sensor_config["data"] do
        import_sensors_of_class(profile_id, to_string(sensor_class), sensor_config["data"])
      end
    end)
  end

  defp import_sensors_of_class(profile_id, sensor_class, sensors) do
    Enum.each(sensors, fn sensor ->
      # Skip sensors without a valid OID (required for SNMP polling)
      # Check for both nil and empty string
      has_valid_oid = present?(sensor["oid"]) || present?(sensor["num_oid"])

      if has_valid_oid do
        attrs = %{
          device_profile_id: profile_id,
          sensor_class: sensor_class,
          oid: nilify_blank(sensor["oid"]),
          num_oid: nilify_blank(sensor["num_oid"]),
          index: to_string(sensor["index"] || ""),
          descr: sensor["descr"],
          divisor: calculate_divisor(sensor["precision"]),
          precision: sensor["precision"],
          sensor_type: sensor["type"],
          low_limit: parse_float(sensor["low_limit"]),
          low_warn_limit: parse_float(sensor["low_warn_limit"]),
          warn_limit: parse_float(sensor["warn_limit"]),
          high_limit: parse_float(sensor["high_limit"]),
          skip_value_lt: parse_float(sensor["skip_value_lt"]),
          skip_value_gt: parse_float(sensor["skip_value_gt"]),
          user_func: sensor["user_func"],
          entPhysicalIndex: sensor["entPhysicalIndex"],
          group: sensor["group"],
          options: extract_sensor_options(sensor)
        }

        import_sensor_with_states(attrs, sensor["states"])
      end
    end)
  end

  defp import_sensor_with_states(attrs, states) do
    case DeviceProfiles.create_sensor_definition(attrs) do
      {:ok, sensor_def} ->
        # Import sensor states if present
        if states do
          import_sensor_states(sensor_def.id, states)
        end

      {:error, reason} ->
        Logger.warning("Failed to import sensor: #{inspect(reason)}")
    end
  end

  defp import_sensor_states(sensor_definition_id, states) when is_list(states) do
    Enum.each(states, fn state ->
      attrs = %{
        sensor_definition_id: sensor_definition_id,
        value: state["value"],
        descr: state["descr"],
        generic: state["generic"],
        graph: state["graph"]
      }

      DeviceProfiles.create_sensor_state(attrs)
    end)
  end

  defp calculate_divisor(nil), do: 1

  defp calculate_divisor(precision) when is_integer(precision) and precision > 0 do
    # Precision in device profiles is the divisor value itself, not an exponent
    # For example: precision: 10 means divide by 10, precision: 1024 means divide by 1024
    precision
  end

  defp calculate_divisor(_), do: 1

  defp parse_float(nil), do: nil
  defp parse_float(value) when is_float(value), do: value
  defp parse_float(value) when is_integer(value), do: value * 1.0

  defp parse_float(value) when is_binary(value) do
    case Float.parse(value) do
      {float, _remainder} -> float
      :error -> nil
    end
  end

  defp parse_float(_), do: nil

  defp extract_sensor_options(sensor) do
    %{
      snmp_flags: sensor["snmp_flags"],
      state_name: sensor["state_name"],
      skip_values: sensor["skip_values"]
    }
    |> Enum.reject(fn {_k, v} -> is_nil(v) end)
    |> Map.new()
  end

  # Check if a value is present (not nil and not empty string)
  defp present?(nil), do: false
  defp present?(""), do: false
  defp present?(_), do: true

  # Convert OID values, preserving all data types since the field is now JSONB
  # Only convert empty strings to nil
  defp nilify_blank(nil), do: nil
  defp nilify_blank(""), do: nil
  defp nilify_blank(value), do: value
end
