defmodule ToweropsWeb.Api.V1.MibController do
  @moduledoc """
  API controller for managing SNMP MIB files.

  Allows uploading MIB files to the persistent storage for use with MIB name translation.

  All endpoints require superuser API token authentication.
  """

  use ToweropsWeb, :controller

  require Logger

  @mib_dir Application.compile_env(:towerops, :mib_dir, "/app/mibs")

  @doc """
  Upload a MIB file or archive (.tar.gz, .zip) containing MIB files.

  Requires superuser API token.

  ## Request

      POST /api/v1/mibs
      Content-Type: multipart/form-data

      file: <mib_file>
      vendor: <vendor_name> (optional)

  ## Response

      201 Created
      {
        "status": "ok",
        "message": "Successfully uploaded MIB file",
        "vendor": "mikrotik",
        "files_count": 15
      }
  """
  def upload(conn, params) do
    with :ok <- require_superuser(conn) do
      case params["file"] do
        %Plug.Upload{} = upload ->
          handle_upload_with_vendor(conn, upload, params)

        _ ->
          conn
          |> put_status(:bad_request)
          |> json(%{error: "Missing file parameter"})
      end
    end
  end

  defp handle_upload_with_vendor(conn, upload, params) do
    vendor = params["vendor"] || "custom"

    # Validate vendor name to prevent directory traversal
    case validate_vendor_name(vendor) do
      :ok ->
        handle_upload(conn, upload, vendor)

      {:error, reason} ->
        conn
        |> put_status(:bad_request)
        |> json(%{error: reason})
    end
  end

  @doc """
  List all available MIB vendors.

  Requires superuser API token.

  ## Response

      200 OK
      {
        "vendors": ["mikrotik", "cisco", "ubiquiti", ...],
        "total_files": 1234
      }
  """
  def index(conn, _params) do
    with :ok <- require_superuser(conn) do
      case list_mib_vendors() do
        {:ok, vendors, total_files} ->
          json(conn, %{
            vendors: vendors,
            total_files: total_files
          })

        {:error, reason} ->
          conn
          |> put_status(:internal_server_error)
          |> json(%{error: inspect(reason)})
      end
    end
  end

  @doc """
  Delete MIB files for a specific vendor.

  Requires superuser API token.

  ## Request

      DELETE /api/v1/mibs/:vendor

  ## Response

      200 OK
      {
        "status": "ok",
        "message": "Deleted MIB files for vendor: mikrotik"
      }
  """
  def delete(conn, %{"vendor" => vendor}) do
    with :ok <- require_superuser(conn) do
      # Validate vendor name to prevent directory traversal
      case validate_vendor_name(vendor) do
        :ok ->
          vendor_dir = Path.join(@mib_dir, vendor)
          delete_vendor_mibs(conn, vendor, vendor_dir)

        {:error, reason} ->
          conn
          |> put_status(:bad_request)
          |> json(%{error: reason})
      end
    end
  end

  defp delete_vendor_mibs(conn, vendor, vendor_dir) do
    if File.exists?(vendor_dir) do
      remove_vendor_directory(conn, vendor, vendor_dir)
    else
      conn
      |> put_status(:not_found)
      |> json(%{error: "Vendor not found: #{vendor}"})
    end
  end

  defp remove_vendor_directory(conn, vendor, vendor_dir) do
    # Vendor directory is safe - constructed from validated vendor name and base MIB directory
    # Validation ensures no path traversal characters (., /, \, :) are present
    # sobelow_skip ["Traversal.FileModule"]
    case File.rm_rf(vendor_dir) do
      {:ok, _files} ->
        Logger.info("Deleted MIB files for vendor: #{vendor}")

        json(conn, %{
          status: "ok",
          message: "Deleted MIB files for vendor: #{vendor}"
        })

      {:error, reason, file} ->
        Logger.error("Failed to delete MIB files for #{vendor}: #{file} - #{inspect(reason)}")

        conn
        |> put_status(:internal_server_error)
        |> json(%{error: "Failed to delete MIB files"})
    end
  end

  # Private functions

  defp require_superuser(conn) do
    user = conn.assigns[:current_user]

    if !user || !user.is_superuser do
      Logger.warning("MIB management attempted by non-superuser: #{inspect(user && user.email)}")

      conn
      |> put_status(:forbidden)
      |> json(%{error: "Superuser access required. Only API tokens created by superusers can manage MIB files."})
      |> halt()
    else
      :ok
    end
  end

  defp handle_upload(conn, upload, vendor) do
    vendor_dir = Path.join(@mib_dir, vendor)
    File.mkdir_p!(vendor_dir)

    cond do
      String.ends_with?(upload.filename, ".tar.gz") or String.ends_with?(upload.filename, ".tgz") ->
        extract_tarball(conn, upload, vendor_dir, vendor)

      String.ends_with?(upload.filename, ".zip") ->
        extract_zip(conn, upload, vendor_dir, vendor)

      true ->
        copy_single_file(conn, upload, vendor_dir, vendor)
    end
  end

  defp extract_tarball(conn, upload, vendor_dir, vendor) do
    case System.cmd("tar", ["-xzf", upload.path, "-C", vendor_dir]) do
      {_output, 0} ->
        files_count = count_files(vendor_dir)
        Logger.info("Extracted #{files_count} MIB files for vendor: #{vendor}")

        conn
        |> put_status(:created)
        |> json(%{
          status: "ok",
          message: "Successfully extracted MIB archive",
          vendor: vendor,
          files_count: files_count
        })

      {error, exit_code} ->
        Logger.error("Failed to extract tarball: #{error}")

        conn
        |> put_status(:bad_request)
        |> json(%{error: "Failed to extract archive (exit code: #{exit_code})"})
    end
  end

  defp extract_zip(conn, upload, vendor_dir, vendor) do
    case System.cmd("unzip", ["-o", upload.path, "-d", vendor_dir]) do
      {_output, 0} ->
        files_count = count_files(vendor_dir)
        Logger.info("Extracted #{files_count} MIB files for vendor: #{vendor}")

        conn
        |> put_status(:created)
        |> json(%{
          status: "ok",
          message: "Successfully extracted MIB archive",
          vendor: vendor,
          files_count: files_count
        })

      {error, exit_code} ->
        Logger.error("Failed to extract zip: #{error}")

        conn
        |> put_status(:bad_request)
        |> json(%{error: "Failed to extract archive (exit code: #{exit_code})"})
    end
  end

  defp copy_single_file(conn, upload, vendor_dir, vendor) do
    # Validate filename to prevent directory traversal
    case validate_filename(upload.filename) do
      :ok ->
        copy_validated_file(conn, upload, vendor_dir, vendor)

      {:error, reason} ->
        Logger.warning("Invalid filename rejected: #{upload.filename} for vendor: #{vendor}")

        conn
        |> put_status(:bad_request)
        |> json(%{error: reason})
    end
  end

  defp copy_validated_file(conn, upload, vendor_dir, vendor) do
    # Use Path.basename to ensure we only use the filename, not any path components
    safe_filename = Path.basename(upload.filename)
    target_path = Path.join(vendor_dir, safe_filename)

    # Check if target already exists as a directory
    if File.dir?(target_path) do
      Logger.warning("Cannot upload MIB file: target path is a directory: #{safe_filename} for vendor: #{vendor}")

      conn
      |> put_status(:bad_request)
      |> json(%{error: "Cannot upload: filename conflicts with existing directory"})
    else
      # upload.path is safe - it's controlled by Plug.Upload, not user input
      # sobelow_skip ["Traversal.FileModule"]
      case File.cp(upload.path, target_path) do
        :ok ->
          Logger.info("Uploaded MIB file: #{upload.filename} for vendor: #{vendor}")

          conn
          |> put_status(:created)
          |> json(%{
            status: "ok",
            message: "Successfully uploaded MIB file",
            vendor: vendor,
            filename: upload.filename
          })

        {:error, :eisdir} ->
          Logger.error("Failed to copy MIB file - target is a directory: #{upload.filename} for vendor: #{vendor}")

          conn
          |> put_status(:bad_request)
          |> json(%{error: "Cannot upload: filename conflicts with existing directory"})

        {:error, reason} ->
          Logger.error("Failed to copy MIB file: #{inspect(reason)}")

          conn
          |> put_status(:internal_server_error)
          |> json(%{error: "Failed to upload file: #{inspect(reason)}"})
      end
    end
  end

  defp list_mib_vendors do
    if File.exists?(@mib_dir) do
      vendors =
        @mib_dir
        |> File.ls!()
        |> Enum.filter(fn name ->
          path = Path.join(@mib_dir, name)
          File.dir?(path) and name != "lost+found"
        end)
        |> Enum.sort()

      total_files = count_files(@mib_dir)

      {:ok, vendors, total_files}
    else
      {:ok, [], 0}
    end
  rescue
    e ->
      Logger.error("Failed to list MIB vendors: #{inspect(e)}")
      {:error, e}
  end

  defp count_files(dir) do
    dir
    |> Path.join("**/*")
    |> Path.wildcard()
    |> Enum.count(&File.regular?/1)
  end

  # Validate vendor name to prevent directory traversal attacks
  # Only allow alphanumeric characters, hyphens, and underscores
  defp validate_vendor_name(vendor) when is_binary(vendor) do
    # Check for directory traversal sequences
    if String.contains?(vendor, [".", "/", "\\", ":"]) do
      {:error, "Invalid vendor name: cannot contain path separators or dots"}
    else
      # Check if vendor name matches safe pattern (alphanumeric, hyphen, underscore)
      if vendor =~ ~r/^[a-zA-Z0-9_-]+$/ do
        :ok
      else
        {:error, "Invalid vendor name: must contain only letters, numbers, hyphens, and underscores"}
      end
    end
  end

  defp validate_vendor_name(_), do: {:error, "Invalid vendor name"}

  # Validate filename to prevent directory traversal
  defp validate_filename(filename) when is_binary(filename) do
    # Check for directory traversal sequences
    if String.contains?(filename, ["..", "/", "\\"]) or String.starts_with?(filename, ".") do
      {:error, "Invalid filename: cannot contain path separators or parent directory references"}
    else
      :ok
    end
  end

  defp validate_filename(_), do: {:error, "Invalid filename"}
end
