require "uri"
require "public_suffix"
require "ipaddr"

module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :authenticated?
  end

  class_methods do
    def allow_unauthenticated_access(**options)
      skip_before_action :require_authentication, **options
    end
  end

  private

  def authenticated?
    resume_session
  end

  def require_authentication
    resume_session || request_authentication
  end

  def resume_session
    Current.session ||= find_session_by_cookie
  end

  def find_session_by_cookie
    Session.active.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
  end

  def request_authentication
    session[:return_to_after_authenticating] = request.url
    redirect_to signin_path
  end

  def after_authentication_url
    session.delete(:return_to_after_authenticating) || root_url
  end

  def start_new_session_for(user, acr: "1", remember_me: false)
    user.update!(last_sign_in_at: Time.current)
    user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip, acr: acr, remember_me: remember_me).tap do |session|
      Current.session = session

      # Extract root domain for cross-subdomain cookies (required for forward auth)
      domain = extract_root_domain(request.host)

      # Set cookie options based on environment
      # Production: Use SameSite=None to allow cross-site cookies (needed for OIDC conformance testing)
      # Development: Use SameSite=Lax since HTTPS might not be available
      cookie_options = if Rails.env.production?
        {
          value: session.id,
          httponly: true,
          same_site: :lax,
          secure: true
        }
      else
        {
          value: session.id,
          httponly: true,
          same_site: :lax,
          secure: false
        }
      end

      # Set domain for cross-subdomain authentication if we can extract it
      cookie_options[:domain] = domain if domain.present?

      # When "Remember me" is off, issue a browser-session cookie (no Expires)
      # so closing the browser signs the user out — especially important on
      # shared devices. The server Session#expires_at still enforces the
      # 24h / 30d window regardless.
      if remember_me
        cookies.signed.permanent[:session_id] = cookie_options
      else
        cookies.signed[:session_id] = cookie_options
      end

      # Create a one-time token for immediate forward auth after authentication
      # This solves the race condition where browser hasn't processed cookie yet
      create_forward_auth_token(session)
    end
  end

  def terminate_session
    Current.session.destroy
    cookies.delete(:session_id)
  end

  # Extract root domain for cross-subdomain cookies in SSO forward_auth system.
  #
  # PURPOSE: Enables a single authentication session to work across multiple subdomains
  # by setting cookies with the domain parameter (e.g., .example.com allows access from
  # both app.example.com and api.example.com).
  #
  # CRITICAL: Returns nil for IP addresses (IPv4 and IPv6) and localhost - this is intentional!
  # When accessing services by IP, there are no subdomains to share cookies with,
  # and setting a domain cookie would break authentication.
  #
  # Uses the Public Suffix List (industry standard maintained by Mozilla) to
  # correctly handle complex domain patterns like co.uk, com.au, appspot.com, etc.
  #
  # Examples:
  # - app.example.com -> .example.com (enables cross-subdomain SSO)
  # - api.example.co.uk -> .example.co.uk (handles complex TLDs)
  # - myapp.appspot.com -> .myapp.appspot.com (handles platform domains)
  # - localhost -> nil (local development, no domain cookie)
  # - 192.168.1.1 -> nil (IP access, no domain cookie - prevents SSO breakage)
  #
  # @param host [String] The request host (may include port)
  # @return [String, nil] Root domain with leading dot for cookies, or nil for no domain setting
  def extract_root_domain(host)
    return nil if host.blank? || host.match?(/^(localhost|127\.0\.0\.1|::1)$/)

    # Strip port number for domain parsing
    host_without_port = host.split(":").first

    # Check if it's an IP address (IPv4 or IPv6) - if so, don't set domain cookie
    begin
      return nil if IPAddr.new(host_without_port)
    rescue
      false
    end

    # Use Public Suffix List for accurate domain parsing
    domain = PublicSuffix.parse(host_without_port)
    ".#{domain.domain}"
  rescue PublicSuffix::DomainInvalid
    # Fallback for invalid domains or IPs
    nil
  end

  # Create a one-time token for forward auth to handle the race condition
  # where the browser hasn't processed the session cookie yet.
  #
  # The token is bound to the destination host so that anyone who observes
  # the token (Referer leaks, access logs, JS monitors) cannot redeem it for
  # a different application within the 60-second TTL.
  def create_forward_auth_token(session_obj)
    controller_session = session
    return unless controller_session[:return_to_after_authenticating].present?

    uri = URI.parse(controller_session[:return_to_after_authenticating])

    # OAuth flow handles its own session propagation — no fa_token needed.
    return if uri.path&.start_with?("/oauth/")

    # Path-only URLs are same-origin on Clinch; the cookie race doesn't apply
    # and we have no destination host to bind against.
    bound_host = uri.hostname&.downcase
    return if bound_host.blank?

    token = SecureRandom.urlsafe_base64(32)
    Rails.cache.write(
      "forward_auth_token:#{token}",
      { session_id: session_obj.id, host: bound_host },
      expires_in: 60.seconds
    )

    query_params = URI.decode_www_form(uri.query || "").to_h
    query_params["fa_token"] = token
    uri.query = URI.encode_www_form(query_params)
    controller_session[:return_to_after_authenticating] = uri.to_s
  end
end
