defmodule Towerops.Accounts.LoginAttempt do
  @moduledoc """
  Schema for tracking login attempts (successful and failed).

  This schema is used for security auditing and GDPR compliance:
  - All login attempts are recorded with IP address, location, and device metadata
  - On user deletion, user_id is set to NULL (anonymization) but IP/location preserved
  - Email addresses are preserved after anonymization for pattern analysis
  - Data retention: 365 days for active records, 90 days for anonymized records
  """

  use Ecto.Schema

  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  @valid_methods ~w(password magic_link mobile_qr)
  @valid_failure_reasons ~w(invalid_credentials invalid_totp invalid_magic_link account_disabled)

  schema "login_attempts" do
    field :email, :string
    field :success, :boolean
    field :method, :string
    field :failure_reason, :string

    # Network/location data
    field :ip_address, :string
    field :user_agent, :string
    field :country_code, :string
    field :country_name, :string
    field :city_name, :string
    field :subdivision_1_name, :string

    # GDPR tracking
    field :anonymized_at, :utc_datetime

    belongs_to :user, Towerops.Accounts.User

    timestamps(type: :utc_datetime, updated_at: false)
  end

  @doc """
  Creates a changeset for recording a login attempt.

  ## Required fields
  - `:success` - Boolean indicating if login was successful
  - `:method` - Login method (password, magic_link, mobile_qr)
  - `:ip_address` - IP address of the login attempt

  ## Conditional validation
  - If `success` is false, `:failure_reason` is required
  - If `success` is true, `:failure_reason` must be nil

  ## Examples

      iex> changeset(%LoginAttempt{}, %{
      ...>   user_id: "123",
      ...>   email: "user@example.com",
      ...>   success: true,
      ...>   method: "password",
      ...>   ip_address: "192.168.1.1",
      ...>   country_name: "United States"
      ...> })
  """
  def changeset(login_attempt, attrs) do
    login_attempt
    |> cast(attrs, [
      :user_id,
      :email,
      :success,
      :method,
      :failure_reason,
      :ip_address,
      :user_agent,
      :country_code,
      :country_name,
      :city_name,
      :subdivision_1_name
    ])
    |> validate_required([:success, :method, :ip_address])
    |> validate_inclusion(:method, @valid_methods)
    |> validate_failure_reason()
    |> validate_length(:email, max: 160)
    |> validate_length(:ip_address, max: 45)
    |> validate_length(:user_agent, max: 1000)
  end

  @doc """
  Anonymizes a login attempt by setting user_id to nil and recording timestamp.

  This is used for GDPR compliance when a user is deleted. The IP address,
  location data, and other metadata are preserved for security analysis.
  """
  def anonymize_changeset(login_attempt) do
    change(login_attempt, %{user_id: nil, anonymized_at: DateTime.utc_now()})
  end

  # Private helpers

  defp validate_failure_reason(changeset) do
    success = get_field(changeset, :success)
    failure_reason = get_field(changeset, :failure_reason)

    cond do
      # If success is true, failure_reason must be nil
      success == true && !is_nil(failure_reason) ->
        add_error(changeset, :failure_reason, "must be nil when login is successful")

      # If success is false, failure_reason is required
      success == false && is_nil(failure_reason) ->
        add_error(changeset, :failure_reason, "is required when login fails")

      # If success is false and failure_reason is present, validate it's in the allowed list
      success == false && !is_nil(failure_reason) ->
        validate_inclusion(changeset, :failure_reason, @valid_failure_reasons)

      # Otherwise, it's valid
      true ->
        changeset
    end
  end
end
