defmodule Towerops.Devices.MikrotikBackups do
  @moduledoc """
  Context for managing MikroTik device configuration backups.
  """

  import Ecto.Query

  alias Towerops.Devices.MikrotikBackup
  alias Towerops.Repo

  @doc """
  Compresses config text using gzip.
  """
  def compress_config(config_text) when is_binary(config_text) do
    :zlib.compress(config_text)
  end

  @doc """
  Decompresses config from binary.
  """
  def decompress_config(compressed_binary) when is_binary(compressed_binary) do
    :zlib.uncompress(compressed_binary)
  end

  @doc """
  Normalizes config by removing comments and whitespace.
  This prevents false positives from RouterOS timestamp comments.
  """
  def normalize_config(config_text) when is_binary(config_text) do
    config_text
    |> String.split("\n")
    |> Enum.reject(&String.starts_with?(&1, "#"))
    |> Enum.map(&String.trim/1)
    |> Enum.reject(&(&1 == ""))
    |> Enum.join("\n")
  end

  @doc """
  Calculates SHA256 hash of raw config text.
  Used for integrity verification.
  """
  def calculate_hash(config_text) when is_binary(config_text) do
    :sha256 |> :crypto.hash(config_text) |> Base.encode16(case: :lower)
  end

  @doc """
  Calculates SHA256 hash of normalized config.
  Used for deduplication - ignores timestamp comments.
  """
  def calculate_hash_normalized(config_text) when is_binary(config_text) do
    config_text
    |> normalize_config()
    |> then(&:crypto.hash(:sha256, &1))
    |> Base.encode16(case: :lower)
  end

  @doc """
  Creates a new backup for a device.
  Compresses config and calculates both raw and normalized hashes.
  """
  def create_backup(device_id, config_text, trigger_source \\ "daily_cron") do
    config_size = byte_size(config_text)
    compressed = compress_config(config_text)
    compressed_size = byte_size(compressed)

    attrs = %{
      device_id: device_id,
      config_compressed: compressed,
      config_hash: calculate_hash(config_text),
      config_hash_normalized: calculate_hash_normalized(config_text),
      config_size_bytes: config_size,
      compressed_size_bytes: compressed_size,
      backed_up_at: DateTime.utc_now(),
      trigger_source: trigger_source
    }

    %MikrotikBackup{}
    |> MikrotikBackup.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Gets the most recent backup for a device.
  Returns nil if no backups exist.
  """
  def get_latest_backup(device_id) do
    Repo.one(from(b in MikrotikBackup, where: b.device_id == ^device_id, order_by: [desc: b.backed_up_at], limit: 1))
  end

  @doc """
  Checks if a device needs a backup by comparing normalized hashes.
  Returns {:ok, true} if backup needed, {:ok, false} if config unchanged.
  """
  def needs_backup?(device_id, config_text) do
    new_hash_normalized = calculate_hash_normalized(config_text)

    case get_latest_backup(device_id) do
      nil ->
        {:ok, true}

      latest ->
        {:ok, latest.config_hash_normalized != new_hash_normalized}
    end
  end

  @doc """
  Lists backup history for a device, most recent first.
  """
  def list_device_backups(device_id, opts \\ []) do
    limit = Keyword.get(opts, :limit, 100)

    Repo.all(from(b in MikrotikBackup, where: b.device_id == ^device_id, order_by: [desc: b.backed_up_at], limit: ^limit))
  end

  @doc """
  Gets a backup by id. Raises if not found.
  """
  def get_backup!(id) do
    Repo.get!(MikrotikBackup, id)
  end

  @doc """
  Deletes a backup.
  """
  def delete_backup(%MikrotikBackup{} = backup) do
    Repo.delete(backup)
  end
end
