defmodule Mix.Tasks.Totp.Diagnose do
  @shortdoc "Diagnose TOTP verification failures"

  @moduledoc """
  Diagnostic tool for TOTP verification failures.

  Usage:
      mix totp.diagnose <user_email> <provided_code> <server_unix_timestamp>

  Example:
      mix totp.diagnose user@example.com 776330 1769641093

  This will:
  1. Check if the provided code matches the user's secret at any time within ±24 hours
  2. Report the exact time offset if found
  3. Help identify if the issue is clock drift or secret mismatch
  """

  use Mix.Task

  require Logger

  def run(args) do
    Mix.Task.run("app.start")

    case args do
      [email, provided_code, timestamp_str] ->
        diagnose(email, provided_code, String.to_integer(timestamp_str))

      _ ->
        IO.puts("""
        Usage: mix totp.diagnose <user_email> <provided_code> <server_unix_timestamp>

        Example:
            mix totp.diagnose user@example.com 776330 1769641093
        """)
    end
  end

  defp diagnose(email, provided_code, server_timestamp) do
    IO.puts("\n=== TOTP Diagnostic Tool ===\n")
    IO.puts("Email: #{email}")
    IO.puts("Provided code: #{provided_code}")
    IO.puts("Server time: #{DateTime.from_unix!(server_timestamp)} (unix: #{server_timestamp})")

    user = Towerops.Accounts.get_user_by_email(email)

    if is_nil(user) do
      IO.puts("\n❌ User not found!")
      exit({:shutdown, 1})
    end

    if is_nil(user.totp_secret) do
      IO.puts("\n❌ User does not have TOTP enabled!")
      exit({:shutdown, 1})
    end

    IO.puts("\nTOTP enabled: Yes")
    IO.puts("Secret length: #{byte_size(user.totp_secret)} bytes")

    # Check current expected codes
    IO.puts("\n--- Expected codes at failure time ---")
    expected_current = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp)
    expected_prev = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp - 30)
    expected_next = NimbleTOTP.verification_code(user.totp_secret, time: server_timestamp + 30)

    IO.puts("Current window: #{expected_current}")
    IO.puts("Previous (-30s): #{expected_prev}")
    IO.puts("Next (+30s): #{expected_next}")

    # Search for when the provided code would have been valid
    IO.puts("\n--- Searching for provided code in wider time window ---")
    IO.puts("Scanning ±24 hours (2880 time steps)...")

    found_time = search_for_code(user.totp_secret, provided_code, server_timestamp)

    if found_time do
      offset_seconds = found_time - server_timestamp
      offset_minutes = Float.round(offset_seconds / 60, 1)

      IO.puts("\n✓ FOUND! The provided code was valid at:")
      IO.puts("  Time: #{DateTime.from_unix!(found_time)}")
      IO.puts("  Offset: #{offset_seconds} seconds (#{offset_minutes} minutes)")

      cond do
        abs(offset_seconds) <= 120 ->
          IO.puts("\n⚠️  This should have been accepted (within ±2 minute window)")
          IO.puts("   Possible bug in verification logic!")

        abs(offset_seconds) <= 900 ->
          IO.puts("\n⏰ Clock drift detected: #{abs(offset_minutes)} minutes")
          IO.puts("   The client's clock is off by more than 2 minutes.")

        true ->
          IO.puts("\n⏰ Extreme clock drift: #{abs(offset_minutes)} minutes")
          IO.puts("   The client's clock is significantly incorrect.")
      end
    else
      IO.puts("\n❌ NOT FOUND in ±24 hour window")
      IO.puts("\nThis strongly suggests a SECRET MISMATCH:")
      IO.puts("  - The secret in the database differs from what's in the authenticator app")
      IO.puts("  - The user may have scanned a different QR code")
      IO.puts("  - The secret may have been regenerated without updating the authenticator")
      IO.puts("\nRecommended action:")
      IO.puts("  1. Have the user disable TOTP")
      IO.puts("  2. Re-enable TOTP and scan the new QR code")
      IO.puts("  3. Verify the new code works before proceeding")
    end

    IO.puts("\n")
  end

  defp search_for_code(secret, provided_code, server_timestamp) do
    # Check ±24 hours (86400 seconds = 2880 time steps of 30 seconds)
    time_steps = 2880

    Enum.find_value(-time_steps..time_steps, fn step ->
      test_time = server_timestamp + step * 30
      expected = NimbleTOTP.verification_code(secret, time: test_time)

      if expected == provided_code do
        test_time
      end
    end)
  end
end
