defmodule Mix.Tasks.GenVendorModules do
  @shortdoc "Generate vendor module skeletons from YAML profiles"

  @moduledoc """
  Generates vendor module skeletons from YAML profiles.

  ## Usage

      mix gen_vendor_modules [--dry-run] [--group GROUP]

  ## Options

    * `--dry-run` - Show what would be generated without creating files
    * `--group GROUP` - Only generate for a specific group/vendor

  ## Examples

      # Generate all missing vendor modules
      mix gen_vendor_modules

      # Preview what would be generated
      mix gen_vendor_modules --dry-run

      # Generate only for a specific vendor
      mix gen_vendor_modules --group cisco
  """

  use Mix.Task

  @profiles_dir "priv/profiles/os_detection"
  @vendors_dir "lib/towerops/snmp/profiles/vendors"

  # Filename prefix to vendor mapping for profiles without group field
  @filename_vendor_map %{
    "cisco" => "cisco",
    "juniper" => "juniper",
    "junos" => "juniper",
    "hp" => "hp",
    "dell" => "dell",
    "ibm" => "ibm",
    "netgear" => "netgear",
    "dlink" => "dlink",
    "d-link" => "dlink",
    "tp-link" => "tplink",
    "tplink" => "tplink",
    "netapp" => "netapp",
    "synology" => "synology",
    "qnap" => "qnap",
    "vmware" => "vmware",
    "palo" => "paloalto",
    "panos" => "paloalto",
    "fortinet" => "fortinet",
    "forti" => "fortinet",
    "checkpoint" => "checkpoint",
    "sonic" => "sonicwall",
    "sonicwall" => "sonicwall",
    "opengear" => "opengear",
    "raritan" => "raritan",
    "eaton" => "eaton",
    "liebert" => "liebert",
    "cyberpower" => "cyberpower",
    "tripp" => "tripplite",
    "axis" => "axis",
    "hikvision" => "hikvision",
    "avaya" => "avaya",
    "polycom" => "polycom",
    "yealink" => "yealink",
    "mitel" => "mitel",
    "riverbed" => "riverbed",
    "citrix" => "citrix",
    "f5" => "f5",
    "a10" => "a10",
    "infoblox" => "infoblox",
    "bluecoat" => "bluecoat",
    "barracuda" => "barracuda",
    "aruba" => "aruba",
    "ruckus" => "ruckus"
  }

  @impl Mix.Task
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [dry_run: :boolean, group: :string])
    dry_run = Keyword.get(opts, :dry_run, false)
    filter_group = Keyword.get(opts, :group)

    # Get existing vendors and covered profiles
    existing_vendors = get_existing_vendors()
    covered_profiles = get_covered_profiles()
    IO.puts("Found #{length(existing_vendors)} existing vendor modules")
    IO.puts("Found #{length(covered_profiles)} profiles already covered")

    # Parse all YAML profiles
    profiles = parse_yaml_profiles()
    IO.puts("Found #{length(profiles)} YAML profiles")

    # Group profiles by vendor
    grouped = group_profiles(profiles)
    IO.puts("Grouped into #{map_size(grouped)} vendors")

    # Filter if requested
    grouped =
      if filter_group do
        Map.take(grouped, [filter_group])
      else
        grouped
      end

    # Filter out existing vendors and groups where all profiles are already covered
    new_vendors =
      grouped
      |> Enum.reject(fn {vendor, vendor_profiles} ->
        module_name = vendor_to_module_name(vendor)
        module_exists = module_name in existing_vendors

        # Check if all profiles in this group are already covered
        profile_names = Enum.map(vendor_profiles, & &1.os)
        all_covered = Enum.all?(profile_names, &(&1 in covered_profiles))

        module_exists or all_covered
      end)
      |> Enum.map(fn {vendor, vendor_profiles} ->
        # Filter out profiles that are already covered
        uncovered = Enum.reject(vendor_profiles, &(&1.os in covered_profiles))
        {vendor, uncovered}
      end)
      |> Enum.reject(fn {_vendor, profiles} -> Enum.empty?(profiles) end)
      |> Map.new()

    IO.puts("#{map_size(new_vendors)} new vendors to generate")

    if map_size(new_vendors) == 0 do
      IO.puts("\nNo new vendors to generate!")
      :ok
    else
      Enum.each(new_vendors, fn {vendor, vendor_profiles} ->
        generate_vendor_module(vendor, vendor_profiles, dry_run)
      end)

      print_instructions(new_vendors, dry_run)
    end
  end

  defp print_instructions(new_vendors, dry_run) do
    if !dry_run do
      IO.puts("\n\nGenerated #{map_size(new_vendors)} vendor modules.")
      IO.puts("Run `mix compile` to check for errors.")
      IO.puts("Run `mix test` to verify.")
      IO.puts("\nDon't forget to:")
      IO.puts("  1. Add aliases to registry.ex")
      IO.puts("  2. Add vendors to @vendors list in registry.ex")
      IO.puts("  3. Write tests for each vendor")
      IO.puts("  4. Add vendor-specific OIDs if available")
    end
  end

  defp get_existing_vendors do
    "#{@vendors_dir}/*.ex"
    |> Path.wildcard()
    |> Enum.map(&Path.basename(&1, ".ex"))
    |> Enum.reject(&(&1 in ["registry", "vendor"]))
    |> Enum.map(&Macro.camelize/1)
  end

  defp get_covered_profiles do
    # Get all profile names already handled by existing vendor modules
    # This requires the application to be started
    _ = Application.ensure_all_started(:towerops)

    Towerops.Snmp.Profiles.Vendors.Registry.list_profile_names()
  rescue
    _ -> []
  end

  defp parse_yaml_profiles do
    "#{@profiles_dir}/*.yaml"
    |> Path.wildcard()
    |> Enum.map(&parse_yaml_file/1)
    |> Enum.reject(&is_nil/1)
  end

  defp parse_yaml_file(path) do
    case YamlElixir.read_from_file(path) do
      {:ok, data} when is_map(data) ->
        build_profile_map(data, path)

      _ ->
        nil
    end
  end

  defp build_profile_map(data, path) do
    os = Map.get(data, "os")

    if os do
      %{
        os: os,
        group: Map.get(data, "group"),
        text: Map.get(data, "text"),
        sys_object_ids: extract_sys_object_ids(data),
        filename: Path.basename(path, ".yaml")
      }
    end
  end

  defp extract_sys_object_ids(data) do
    data
    |> Map.get("discovery", [])
    |> List.wrap()
    |> Enum.flat_map(&get_sys_object_id_list/1)
  end

  defp get_sys_object_id_list(%{"sysObjectID" => oids}) when is_list(oids), do: oids
  defp get_sys_object_id_list(_), do: []

  defp group_profiles(profiles) do
    profiles
    |> Enum.group_by(&determine_vendor/1)
    |> Enum.reject(fn {vendor, _} -> vendor == nil end)
    |> Map.new()
  end

  defp determine_vendor(%{group: group}) when is_binary(group) and group != "" do
    String.downcase(group)
  end

  defp determine_vendor(%{filename: filename}) do
    infer_vendor_from_filename(filename)
  end

  defp infer_vendor_from_filename(filename) do
    Enum.find_value(@filename_vendor_map, fn {prefix, vendor} ->
      if String.starts_with?(filename, prefix) or String.contains?(filename, prefix) do
        vendor
      end
    end)
  end

  defp vendor_to_module_name(vendor) do
    name =
      vendor
      |> String.replace(~r/[^a-z0-9]+/i, "_")
      |> String.trim("_")
      |> Macro.camelize()

    # Prefix with "N" if starts with a number (e.g., "4rf" -> "N4rf")
    if String.match?(name, ~r/^[0-9]/) do
      "N#{name}"
    else
      name
    end
  end

  defp vendor_to_filename(vendor) do
    name =
      vendor
      |> String.replace(~r/[^a-z0-9]+/i, "_")
      |> String.downcase()
      |> String.trim("_")

    # Prefix with "n" if starts with a number
    if String.match?(name, ~r/^[0-9]/) do
      "n#{name}"
    else
      name
    end
  end

  defp generate_vendor_module(vendor, profiles, dry_run) do
    module_name = vendor_to_module_name(vendor)
    filename = vendor_to_filename(vendor)
    profile_names = profiles |> Enum.map(& &1.os) |> Enum.sort() |> Enum.uniq()

    # Get enterprise OID from first sysObjectID if available
    enterprise_oid = extract_enterprise_oid(profiles)

    content = generate_module_content(module_name, profile_names, enterprise_oid, vendor)

    if dry_run do
      IO.puts("\n--- #{filename}.ex (#{module_name}) ---")
      IO.puts("  Profiles: #{Enum.join(profile_names, ", ")}")
      IO.puts("  Enterprise OID: #{enterprise_oid || "none"}")
    else
      path = "#{@vendors_dir}/#{filename}.ex"
      File.write!(path, content)
      IO.puts("Generated: #{path}")
    end
  end

  defp extract_enterprise_oid(profiles) do
    profiles
    |> Enum.flat_map(& &1.sys_object_ids)
    |> Enum.find_value(fn oid ->
      # Parse enterprise OID from sysObjectID pattern like ".1.3.6.1.4.1.9."
      case Regex.run(~r/\.?1\.3\.6\.1\.4\.1\.(\d+)/, oid) do
        [_, enterprise] -> "1.3.6.1.4.1.#{enterprise}"
        _ -> nil
      end
    end)
  end

  defp generate_module_content(module_name, profile_names, enterprise_oid, vendor) do
    profiles_list = inspect(profile_names, pretty: true, limit: :infinity)

    enterprise_comment =
      if enterprise_oid do
        "  # Enterprise OID: #{enterprise_oid}"
      else
        "  # Enterprise OID: unknown - uses standard MIB OIDs"
      end

    """
    defmodule Towerops.Snmp.Profiles.Vendors.#{module_name} do
      @moduledoc \"\"\"
      SNMP profile for #{String.capitalize(vendor)} devices.

      Auto-generated from YAML profiles. Add vendor-specific OIDs as needed.
      \"\"\"

      @behaviour Towerops.Snmp.Profiles.Vendors.Vendor

      alias Towerops.Snmp.Client
      alias Towerops.Snmp.Profiles.Vendors.Vendor

    #{enterprise_comment}

      @impl true
      def profile_names, do: #{profiles_list}

      @impl true
      def detect_hardware(client_opts) do
        case Client.get(client_opts, "1.3.6.1.2.1.1.1.0") do
          {:ok, descr} when is_binary(descr) ->
            parse_hardware_from_descr(descr)

          _ ->
            nil
        end
      end

      defp parse_hardware_from_descr(descr) do
        # TODO: Add vendor-specific hardware detection
        # For now, return first line or word from sysDescr
        descr
        |> String.split(~r/[\\r\\n]/)
        |> List.first()
        |> case do
          nil -> nil
          line -> String.slice(line, 0, 64)
        end
      end

      @impl true
      def wireless_oid_defs do
        [
          # Standard HOST-RESOURCES-MIB CPU
          %{
            oid: "1.3.6.1.2.1.25.3.3.1.2.1",
            sensor_descr: "CPU Load",
            sensor_type: "load",
            sensor_unit: "%",
            sensor_divisor: 1
          }
          # TODO: Add vendor-specific OIDs
        ]
      end

      @impl true
      def discover_wireless_sensors(client_opts) do
        Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
      end
    end
    """
  end
end
