defmodule Mix.Tasks.UploadMibs do
  @shortdoc "Upload MIB files to production API"

  @moduledoc """
  Uploads MIB files from a source directory to the production API.

  ## Usage

      # Upload all vendor MIB directories
      mix upload_mibs --source-path ~/librenms/mibs --token YOUR_SUPERUSER_TOKEN

      # Upload to specific environment
      mix upload_mibs --source-path ~/librenms/mibs --token TOKEN --api-url https://towerops.net

      # Upload specific vendors only
      mix upload_mibs --source-path ~/librenms/mibs --token TOKEN --vendors mikrotik,cisco,ubiquiti

  ## Options

    * `--source-path` - Path to the directory containing MIB files (required)
    * `--token` - Superuser API token for authentication (required)
    * `--api-url` - API base URL (default: https://towerops.net)
    * `--vendors` - Comma-separated list of vendor directories to upload (default: all)

  ## Notes

    * Creates temporary tar.gz archives for each vendor directory
    * Uploads via multipart/form-data to POST /api/v1/mibs
    * Requires a superuser API token
    * Cleans up temporary archives after upload
  """

  use Mix.Task

  require Logger

  @default_api_url "https://towerops.net"

  def run(args) do
    # Start required applications
    {:ok, _} = Application.ensure_all_started(:req)

    {opts, _} =
      OptionParser.parse!(args,
        strict: [
          source_path: :string,
          token: :string,
          api_url: :string,
          vendors: :string
        ]
      )

    source_path = opts[:source_path] || raise "Missing --source-path argument"
    token = opts[:token] || raise "Missing --token argument"
    api_url = opts[:api_url] || @default_api_url
    vendors = parse_vendors(opts[:vendors])

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

    case vendors do
      :all ->
        upload_all_vendors(source_path, api_url, token)

      vendor_list ->
        upload_specific_vendors(source_path, api_url, token, vendor_list)
    end
  end

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

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

  defp upload_all_vendors(source_path, api_url, token) do
    Logger.info("Discovering vendor directories in #{source_path}...")

    vendors =
      source_path
      |> File.ls!()
      |> Enum.filter(fn name ->
        path = Path.join(source_path, name)
        File.dir?(path) and name != "lost+found" and !String.starts_with?(name, ".")
      end)
      |> Enum.sort()

    Logger.info("Found #{length(vendors)} vendor directories")
    upload_vendor_list(source_path, api_url, token, vendors)
  end

  defp upload_specific_vendors(source_path, api_url, token, vendors) do
    Logger.info("Uploading #{length(vendors)} specified vendors")
    upload_vendor_list(source_path, api_url, token, vendors)
  end

  defp upload_vendor_list(source_path, api_url, token, vendors) do
    results =
      Enum.map(vendors, fn vendor ->
        vendor_path = Path.join(source_path, vendor)

        if File.exists?(vendor_path) do
          upload_vendor(api_url, token, vendor, vendor_path)
        else
          Logger.warning("Vendor directory not found: #{vendor}")
          {:error, vendor, "Directory not found"}
        end
      end)

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

    Logger.info("Upload complete: #{successes} succeeded, #{failures} failed")

    if failures > 0 do
      Logger.error("Failed uploads:")

      Enum.each(results, fn
        {:error, vendor, reason} ->
          Logger.error("  - #{vendor}: #{reason}")

        _ ->
          :ok
      end)
    end
  end

  defp upload_vendor(api_url, token, vendor, vendor_path) do
    Logger.info("Uploading #{vendor}...")

    # Create temporary tar.gz archive
    temp_dir = System.tmp_dir!()
    archive_name = "#{vendor}.tar.gz"
    archive_path = Path.join(temp_dir, archive_name)

    try do
      # Create tar.gz archive
      case create_tarball(vendor_path, archive_path) do
        :ok ->
          # Upload to API
          upload_tarball(api_url, token, vendor, archive_path, archive_name)

        {:error, reason} ->
          Logger.error("Failed to create archive for #{vendor}: #{inspect(reason)}")
          {:error, vendor, "Archive creation failed"}
      end
    after
      # Clean up temporary archive
      File.rm(archive_path)
    end
  end

  defp create_tarball(source_dir, archive_path) do
    parent_dir = Path.dirname(source_dir)
    vendor_name = Path.basename(source_dir)

    case System.cmd(
           "tar",
           ["-czf", archive_path, "-C", parent_dir, vendor_name],
           stderr_to_stdout: true
         ) do
      {_output, 0} ->
        :ok

      {error, exit_code} ->
        Logger.error("tar failed (exit #{exit_code}): #{error}")
        {:error, :tar_failed}
    end
  rescue
    e in ErlangError ->
      Logger.error("tar command not found: #{inspect(e)}")
      {:error, :tar_not_found}
  end

  defp upload_tarball(api_url, token, vendor, archive_path, archive_name) do
    url = "#{api_url}/api/v1/mibs"

    Logger.debug("Uploading #{archive_name} to #{url}")

    # Create multipart form with file
    file_content = File.read!(archive_path)

    form = [
      {"vendor", vendor},
      {"file", {file_content, filename: archive_name, content_type: "application/gzip"}}
    ]

    case Req.post(url,
           auth: {:bearer, token},
           form_multipart: form,
           receive_timeout: 120_000
         ) do
      {:ok, %{status: 201, body: body}} when is_map(body) ->
        files_count = Map.get(body, "files_count", "unknown")
        Logger.info("✓ #{vendor}: uploaded #{files_count} files")
        {:ok, vendor, files_count}

      {:ok, %{status: status, body: body}} when is_map(body) ->
        error = Map.get(body, "error", "Unknown error")

        # For 403, log the full error message since it's likely a permission issue
        if status == 403 do
          Logger.error("✗ #{vendor}: HTTP #{status} - #{error}")
          Logger.error("  Make sure your API token has superuser permissions!")
        else
          Logger.error("✗ #{vendor}: HTTP #{status} - #{error}")
        end

        {:error, vendor, "HTTP #{status}: #{error}"}

      {:ok, %{status: status, body: body}} ->
        # Non-JSON response (likely HTML error page)
        error_preview = body |> to_string() |> String.slice(0, 100)
        Logger.error("✗ #{vendor}: HTTP #{status} - #{error_preview}...")
        {:error, vendor, "HTTP #{status}: Non-JSON response"}

      {:error, reason} ->
        Logger.error("✗ #{vendor}: Request failed - #{inspect(reason)}")
        {:error, vendor, "Request failed: #{inspect(reason)}"}
    end
  end
end
