defmodule Towerops.Devices.MikrotikBackups.Differ do
  @moduledoc """
  Provides functionality for generating diffs between MikroTik configuration backups
  and masking sensitive data in configurations.
  """

  @doc """
  Masks sensitive data in a MikroTik configuration.

  Patterns masked:
  - User passwords (password=xxx)
  - SNMP community strings
  - IPsec secrets (secret=xxx)
  - Wireless WPA/WPA2 pre-shared keys

  Each sensitive value is replaced with ***MASKED:hash*** where hash is a short
  SHA256 hash of the original value to allow detecting changes.

  ## Examples

      iex> Differ.mask_sensitive_data("/user set admin password=secret123")
      "/user set admin password=***MASKED:a1b2c3d4***"
  """
  def mask_sensitive_data(config_text) when is_binary(config_text) do
    config_text
    |> mask_passwords()
    |> mask_snmp_communities()
    |> mask_ipsec_secrets()
    |> mask_wireless_keys()
  end

  @doc """
  Generates a unified diff between two configurations.

  Uses the system `diff` command to generate a unified diff format.
  Returns {:ok, diff_string} or {:error, reason}.

  ## Examples

      iex> Differ.generate_unified_diff("config1", "config2")
      {:ok, "--- a\\n+++ b\\n@@ -1 +1 @@\\n-config1\\n+config2\\n"}
  """
  def generate_unified_diff(config_a, config_b) do
    # Create temporary files for diff
    tmp_a = Path.join(System.tmp_dir!(), "mikrotik_config_a_#{:erlang.unique_integer([:positive])}")
    tmp_b = Path.join(System.tmp_dir!(), "mikrotik_config_b_#{:erlang.unique_integer([:positive])}")

    try do
      File.write!(tmp_a, config_a)
      File.write!(tmp_b, config_b)

      # Run diff command with unified format
      case System.cmd("diff", ["-u", tmp_a, tmp_b], stderr_to_stdout: true) do
        {_output, 0} ->
          # Exit code 0 means files are identical
          {:ok, ""}

        {output, 1} ->
          # Exit code 1 means files differ (normal for diff)
          # Remove the file path headers and use generic a/b
          diff =
            output
            |> String.replace(~r/^--- #{Regex.escape(tmp_a)}.*$/m, "--- a")
            |> String.replace(~r/^\+\+\+ #{Regex.escape(tmp_b)}.*$/m, "+++ b")

          {:ok, diff}

        {output, _exit_code} ->
          # Other exit codes indicate errors
          {:error, "diff command failed: #{output}"}
      end
    rescue
      error ->
        {:error, "Failed to generate diff: #{inspect(error)}"}
    after
      File.rm(tmp_a)
      File.rm(tmp_b)
    end
  end

  @doc """
  Calculates statistics from a unified diff.

  Returns a map with:
  - :additions - Number of lines added
  - :deletions - Number of lines deleted
  - :changes - Number of lines modified (min of additions and deletions)

  ## Examples

      iex> Differ.calculate_diff_stats("@@ -1 +1 @@\\n-old\\n+new\\n")
      %{additions: 1, deletions: 1, changes: 1}
  """
  def calculate_diff_stats(diff) when is_binary(diff) do
    lines = String.split(diff, "\n")

    additions = Enum.count(lines, &(String.starts_with?(&1, "+") && !String.starts_with?(&1, "+++")))
    deletions = Enum.count(lines, &(String.starts_with?(&1, "-") && !String.starts_with?(&1, "---")))

    # Changes represent modified lines (paired additions/deletions)
    changes = min(additions, deletions)

    %{
      additions: additions,
      deletions: deletions,
      changes: changes
    }
  end

  # Private functions for masking different types of sensitive data

  defp mask_passwords(config) do
    # Match password= followed by non-whitespace characters
    # Supports both "password=" and variations with quotes
    Regex.replace(
      ~r/(password=)([^\s]+)/i,
      config,
      fn _, prefix, value ->
        hash = hash_value(value)
        "#{prefix}***MASKED:#{hash}***"
      end
    )
  end

  defp mask_snmp_communities(config) do
    # SNMP community strings in RouterOS
    # Mask both the community identifier and name= parameter
    config
    |> mask_snmp_set_identifier()
    |> mask_snmp_name_parameter()
  end

  defp mask_snmp_set_identifier(config) do
    # Pattern: /snmp community set [name] ...
    Regex.replace(
      ~r/(\/snmp\s+community\s+set\s+)([^\s]+)/,
      config,
      fn _, prefix, value ->
        # Don't mask common default names, but mask custom ones
        if value in ["public", "private"] do
          prefix <> value
        else
          hash = hash_value(value)
          "#{prefix}***MASKED:#{hash}***"
        end
      end
    )
  end

  defp mask_snmp_name_parameter(config) do
    # Pattern: name=xxx in SNMP community commands
    Regex.replace(
      ~r/(\/snmp\s+community.*?name=)([^\s]+)/,
      config,
      fn _, prefix, value ->
        # Don't mask common default names
        if value in ["public", "private"] do
          prefix <> value
        else
          hash = hash_value(value)
          "#{prefix}***MASKED:#{hash}***"
        end
      end
    )
  end

  defp mask_ipsec_secrets(config) do
    # IPsec secrets: secret=xxx
    Regex.replace(
      ~r/(secret=)([^\s]+)/i,
      config,
      fn _, prefix, value ->
        hash = hash_value(value)
        "#{prefix}***MASKED:#{hash}***"
      end
    )
  end

  defp mask_wireless_keys(config) do
    # Wireless WPA/WPA2 pre-shared keys
    config
    |> mask_wpa_key("wpa-pre-shared-key")
    |> mask_wpa_key("wpa2-pre-shared-key")
  end

  defp mask_wpa_key(config, key_name) do
    pattern = ~r/(#{key_name}=)([^\s]+)/i

    Regex.replace(pattern, config, fn _, prefix, value ->
      hash = hash_value(value)
      "#{prefix}***MASKED:#{hash}***"
    end)
  end

  # Generate a short hash of a value for change detection
  defp hash_value(value) do
    :sha256
    |> :crypto.hash(value)
    |> Base.encode16(case: :lower)
    |> String.slice(0..7)
  end
end
