defmodule Towerops.Security.BruteForce do
  @moduledoc """
  Context module for brute force protection and IP blocking.

  Handles detection of scanning behavior (5+ unique 404s within 1 minute),
  progressive ban escalation, whitelist management, and integration with
  Cloudflare WAF for permanent bans.
  """

  import Ecto.Query, warn: false

  alias Phoenix.PubSub
  alias Towerops.Repo
  alias Towerops.Security.FourOhFourTracker
  alias Towerops.Security.IpBlock
  alias Towerops.Security.IpWhitelist
  alias Towerops.Workers.CloudflareBanWorker

  require Logger

  @forgiveness_period_days 30

  ## Whitelist Functions

  @doc """
  Checks if an IP address is whitelisted.

  Supports both individual IPs and CIDR ranges.
  Uses ETS cache for performance.
  """
  def check_whitelist(ip_address) do
    whitelist = get_cached_whitelist()
    Enum.any?(whitelist, &ip_matches_entry?(ip_address, &1))
  end

  defp ip_matches_entry?(ip_address, entry) do
    if String.contains?(entry.ip_or_cidr, "/") do
      # CIDR range - need to parse both the range and IP address
      with {:ok, range} <- InetCidr.parse_cidr(entry.ip_or_cidr),
           {:ok, addr} <- :inet.parse_address(String.to_charlist(ip_address)) do
        InetCidr.contains?(range, addr)
      else
        _ -> false
      end
    else
      # Exact IP match
      entry.ip_or_cidr == ip_address
    end
  end

  @doc """
  Lists all whitelisted IPs and CIDR ranges.
  """
  def list_whitelist do
    Repo.all(from w in IpWhitelist, preload: [:added_by], order_by: [desc: w.inserted_at])
  end

  @doc """
  Adds an IP address or CIDR range to the whitelist.
  """
  def add_to_whitelist(ip_or_cidr, description, user) do
    attrs = %{
      ip_or_cidr: ip_or_cidr,
      description: description,
      added_by_id: user.id
    }

    result =
      %IpWhitelist{}
      |> IpWhitelist.changeset(attrs)
      |> Repo.insert()

    case result do
      {:ok, _} ->
        clear_whitelist_cache()
        PubSub.broadcast(Towerops.PubSub, "security:whitelist", :whitelist_updated)

      {:error, _} ->
        :ok
    end

    result
  end

  @doc """
  Removes an IP or CIDR range from the whitelist.
  """
  def remove_from_whitelist(id) do
    case Repo.get(IpWhitelist, id) do
      nil ->
        {:error, :not_found}

      entry ->
        Repo.delete(entry)
        clear_whitelist_cache()
        PubSub.broadcast(Towerops.PubSub, "security:whitelist", :whitelist_updated)
        :ok
    end
  end

  ## Ban Management Functions

  @doc """
  Checks if an IP address is currently banned.

  Returns `:allowed` if not banned or ban expired.
  Returns `{:blocked, banned_until}` if currently banned.
  """
  def check_ban_status(ip_address) do
    case get_by_ip(ip_address) do
      nil ->
        :allowed

      %IpBlock{banned_until: nil, offense_count: count} when count >= 3 ->
        # Permanent ban
        {:blocked, nil}

      %IpBlock{banned_until: banned_until} ->
        if DateTime.after?(DateTime.utc_now(), banned_until) do
          # Ban expired
          :allowed
        else
          {:blocked, banned_until}
        end
    end
  end

  @doc """
  Creates a new ban or escalates an existing ban.

  Escalation schedule:
  - 1st offense: 5 minutes
  - 2nd offense (within 30 days): 1 hour
  - 3rd+ offense (within 30 days): Permanent + Cloudflare push

  Resets to 1st offense if 30 days have passed since last violation.
  """
  def create_or_escalate_ban(ip_address, reason \\ "Automated scanning - 5 unique 404s") do
    case get_by_ip(ip_address) do
      nil ->
        # First offense
        create_ban(ip_address, 1, DateTime.add(DateTime.utc_now(), 5, :minute), reason)

      block ->
        if DateTime.diff(DateTime.utc_now(), block.last_violation_at, :day) > @forgiveness_period_days do
          # Reset after forgiveness period
          update_ban(block, 1, DateTime.add(DateTime.utc_now(), 5, :minute), reason)
        else
          # Escalate
          escalate_ban(block, reason)
        end
    end
  end

  @doc """
  Manually unblocks an IP address.

  Returns `{:ok, ip_block}` if successful.
  Returns `{:error, :not_found}` if IP is not blocked.
  """
  def manually_unblock(ip_address) do
    case get_by_ip(ip_address) do
      nil ->
        {:error, :not_found}

      block ->
        # Delete the block record entirely
        Repo.delete(block)
        Logger.info("Manually unblocked IP: #{ip_address}")
        PubSub.broadcast(Towerops.PubSub, "security:blocks", :blocks_updated)
        {:ok, block}
    end
  end

  @doc """
  Lists blocked IPs with optional filtering.

  Filter options: `:all`, `:permanent`, `:temporary`
  """
  def list_blocked_ips(filter \\ :all) do
    query =
      case filter do
        :permanent ->
          from b in IpBlock,
            where: is_nil(b.banned_until) and b.offense_count >= 3,
            order_by: [desc: b.last_violation_at]

        :temporary ->
          from b in IpBlock,
            where: not is_nil(b.banned_until),
            order_by: [desc: b.last_violation_at]

        :all ->
          from b in IpBlock, order_by: [desc: b.last_violation_at]
      end

    Repo.all(query)
  end

  ## 404 Tracking Functions

  @doc """
  Records a 404 path for an IP address.

  Tracks unique paths in Redis with 60-second TTL.
  Triggers ban creation/escalation if threshold (5) is reached.
  """
  def record_404_path(ip_address, path) do
    FourOhFourTracker.record_404(ip_address, path)
  end

  @doc """
  Gets the count of unique 404 paths for an IP address within the tracking window.
  """
  def get_unique_404_count(ip_address) do
    FourOhFourTracker.get_count(ip_address)
  end

  ## Cleanup Functions

  @doc """
  Deletes expired temporary bans.

  Called by ExpiredBanCleanupWorker hourly.
  """
  def delete_expired_bans do
    {count, _} =
      Repo.delete_all(
        from(b in IpBlock,
          where: not is_nil(b.banned_until),
          where: b.banned_until < ^DateTime.utc_now()
        )
      )

    if count > 0 do
      Logger.info("Deleted #{count} expired ban(s)")
      PubSub.broadcast(Towerops.PubSub, "security:blocks", :blocks_updated)
    end

    :ok
  end

  @doc """
  Deletes old violation records that are not permanent bans.

  Removes records older than 90 days with offense_count < 3.
  Called by StaleViolationCleanupWorker daily.
  """
  def delete_stale_violations do
    cutoff = DateTime.add(DateTime.utc_now(), -90, :day)

    {count, _} = Repo.delete_all(from(b in IpBlock, where: b.last_violation_at < ^cutoff, where: b.offense_count < 3))

    if count > 0 do
      Logger.info("Deleted #{count} stale violation record(s)")
    end

    :ok
  end

  ## Private Functions

  defp get_by_ip(ip_address) do
    Repo.get_by(IpBlock, ip_address: ip_address)
  end

  defp create_ban(ip_address, offense_count, banned_until, reason) do
    attrs = %{
      ip_address: ip_address,
      offense_count: offense_count,
      banned_until: banned_until,
      last_violation_at: DateTime.utc_now(),
      reason: reason
    }

    result =
      %IpBlock{}
      |> IpBlock.changeset(attrs)
      |> Repo.insert()

    case result do
      {:ok, block} ->
        Logger.warning("IP banned (offense #{offense_count}): #{ip_address} until #{banned_until}")
        PubSub.broadcast(Towerops.PubSub, "security:blocks", :blocks_updated)
        {:ok, block}

      error ->
        error
    end
  end

  defp update_ban(block, offense_count, banned_until, reason) do
    attrs = %{
      offense_count: offense_count,
      banned_until: banned_until,
      last_violation_at: DateTime.utc_now(),
      reason: reason
    }

    result =
      block
      |> IpBlock.changeset(attrs)
      |> Repo.update()

    case result do
      {:ok, updated} ->
        Logger.warning("IP ban reset (offense #{offense_count}): #{block.ip_address} until #{banned_until}")
        PubSub.broadcast(Towerops.PubSub, "security:blocks", :blocks_updated)
        {:ok, updated}

      error ->
        error
    end
  end

  defp escalate_ban(block, reason) do
    new_count = block.offense_count + 1

    {banned_until, log_msg} =
      case new_count do
        2 ->
          until = DateTime.add(DateTime.utc_now(), 1, :hour)
          {"#{until}", "1 hour"}

        _ ->
          # Permanent ban
          {nil, "permanent"}
      end

    attrs = %{
      offense_count: new_count,
      banned_until: banned_until,
      last_violation_at: DateTime.utc_now(),
      reason: reason
    }

    result =
      block
      |> IpBlock.changeset(attrs)
      |> Repo.update()

    case result do
      {:ok, updated} ->
        Logger.warning("IP ban escalated to #{log_msg} (offense #{new_count}): #{block.ip_address}")
        PubSub.broadcast(Towerops.PubSub, "security:blocks", :blocks_updated)

        # Push to Cloudflare for permanent bans
        if new_count >= 3 do
          CloudflareBanWorker.enqueue(block.ip_address)
        end

        {:ok, updated}

      error ->
        error
    end
  end

  # Whitelist caching using ETS
  @whitelist_cache_table :brute_force_whitelist_cache

  defp get_cached_whitelist do
    case :ets.whereis(@whitelist_cache_table) do
      :undefined ->
        create_cache_table()
        load_whitelist()

      _ ->
        case :ets.lookup(@whitelist_cache_table, :whitelist) do
          [{:whitelist, entries}] -> entries
          [] -> load_whitelist()
        end
    end
  end

  defp create_cache_table do
    :ets.new(@whitelist_cache_table, [:set, :public, :named_table, read_concurrency: true])
  rescue
    ArgumentError ->
      # Table already exists due to race condition in async tests - this is fine
      :ets.whereis(@whitelist_cache_table)
  end

  defp load_whitelist do
    entries = Repo.all(IpWhitelist)
    :ets.insert(@whitelist_cache_table, {:whitelist, entries})
    entries
  end

  defp clear_whitelist_cache do
    case :ets.whereis(@whitelist_cache_table) do
      :undefined -> :ok
      _ -> :ets.delete(@whitelist_cache_table, :whitelist)
    end
  end
end
