class OidcAuthController < ApplicationController
  allow_unauthenticated_access only: [:authorize, :callback]

  # POST /auth/oidc - Initiate OIDC flow
  def authorize
    # Try PKCE first, fallback gracefully if not supported
    auth_params = {
      scope: [:openid, :email, :profile],
      state: generate_state_token,
      nonce: generate_nonce
    }

    # Add PKCE parameters if supported
    if pkce_supported?
      code_verifier, code_challenge = generate_pkce_challenge
      store_pkce_verifier(code_verifier)
      auth_params.merge!({
        code_challenge: code_challenge,
        code_challenge_method: 'S256'
      })
    end

    redirect_to oidc_client.authorization_uri(auth_params), allow_other_host: true
  end

  # GET /auth/oidc/callback - Handle provider callback
  def callback
    # Verify state token
    unless valid_state_token?(params[:state])
      redirect_to new_session_path, alert: "Invalid authentication state"
      return
    end

    # Store expected nonce for validation
    expected_nonce = session[:oidc_nonce]
    session.delete(:oidc_nonce) # Clear nonce after use

    # Exchange authorization code for tokens
    oidc_client.authorization_code = params[:code]

    # Add PKCE verifier if available
    code_verifier = retrieve_pkce_verifier
    oidc_client.code_verifier = code_verifier if code_verifier.present?

    access_token = oidc_client.access_token!

    # Extract claims from ID token (JWT-only approach)
    id_token = access_token.id_token
    unless id_token.present?
      redirect_to new_session_path, alert: "No ID token received from provider"
      return
    end

    # Validate ID token signature and claims
    unless validate_id_token(id_token, expected_nonce)
      redirect_to new_session_path, alert: "Invalid ID token received"
      return
    end

    # Extract user claims from JWT
    claims = extract_claims_from_id_token(id_token)

    # Find user by email
    user = User.find_by(email_address: claims[:email])

    unless user
      redirect_to new_session_path, alert: "No user found with email: #{claims[:email]}"
      return
    end

    # Update role based on OIDC groups if present
    if claims[:groups].present?
      user.update_role_from_oidc_groups(claims[:groups])
    end

    start_new_session_for(user)
    redirect_to root_path, notice: "Successfully signed in via OIDC"

  rescue OpenIDConnect::Exception, Rack::OAuth2::Client::Error => e
    error_message = handle_oidc_error(e, "token exchange")
    redirect_to new_session_path, alert: error_message
  rescue => e
    error_message = handle_oidc_error(e, "callback processing")
    redirect_to new_session_path, alert: error_message
  end

  private

  def oidc_client
    @oidc_client ||= begin
      # Strip .well-known/openid-configuration if present (discover! adds it automatically)
      issuer_url = ENV['OIDC_DISCOVERY_URL'].sub(%r{/?\.well-known/openid-configuration/?$}, '').chomp('/')

      # Fetch discovery document with validation disabled
      config_url = "#{issuer_url}/.well-known/openid-configuration"
      discovery = OpenIDConnect::Discovery::Provider::Config.discover!(issuer_url)

      OpenIDConnect::Client.new(
        identifier: ENV['OIDC_CLIENT_ID'],
        secret: ENV['OIDC_CLIENT_SECRET'],
        redirect_uri: ENV['OIDC_REDIRECT_URI'] || oidc_callback_url,
        authorization_endpoint: discovery.authorization_endpoint,
        token_endpoint: discovery.token_endpoint,
        userinfo_endpoint: discovery.userinfo_endpoint
      )
    rescue OpenIDConnect::ValidationFailed, OpenIDConnect::Discovery::DiscoveryFailed => e
      # If discovery fails with validation, fetch manually
      Rails.logger.warn "OIDC discovery validation failed: #{e.message}. Fetching manually."

      response = Faraday.get(config_url)
      config = JSON.parse(response.body)

      OpenIDConnect::Client.new(
        identifier: ENV['OIDC_CLIENT_ID'],
        secret: ENV['OIDC_CLIENT_SECRET'],
        redirect_uri: ENV['OIDC_REDIRECT_URI'] || oidc_callback_url,
        authorization_endpoint: config['authorization_endpoint'],
        token_endpoint: config['token_endpoint'],
        userinfo_endpoint: config['userinfo_endpoint']
      )
    end
  end

  def generate_state_token
    token = SecureRandom.hex(32)
    session[:oidc_state] = token
    token
  end

  def generate_nonce
    nonce = SecureRandom.hex(32)
    session[:oidc_nonce] = nonce
    nonce
  end

  def valid_state_token?(state)
    state.present? && session[:oidc_state] == state
  end

  def oidc_callback_url
    "#{request.base_url}/auth/oidc/callback"
  end

  # PKCE (Proof Key for Code Exchange) support
  def pkce_supported?
    # Default to trying PKCE, can be configured via environment if needed
    ENV['OIDC_PKCE_ENABLED'] != 'false'
  end

  def generate_pkce_challenge
    # Generate code verifier: 43-128 characters from [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
    code_verifier = SecureRandom.urlsafe_base64(64).tr('=_-', '')[0, 128]

    # Generate code challenge: BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
    digest = OpenSSL::Digest::SHA256.new
    code_challenge = Base64.urlsafe_encode64(digest.digest(code_verifier)).tr('=', '')

    [code_verifier, code_challenge]
  end

  def store_pkce_verifier(code_verifier)
    session[:oidc_code_verifier] = code_verifier
  end

  def retrieve_pkce_verifier
    verifier = session[:oidc_code_verifier]
    session.delete(:oidc_code_verifier) # Clear after use
    verifier
  end

  # JWT claim extraction and validation
  def extract_claims_from_id_token(id_token)
    # Decode JWT without verification first to get claims
    decoded_jwt = JWT.decode(id_token, nil, false).first

    {
      sub: decoded_jwt['sub'],
      email: decoded_jwt['email'],
      name: decoded_jwt['name'],
      email_verified: decoded_jwt['email_verified'],
      groups: decoded_jwt['groups'] || decoded_jwt['memberof'],
      nonce: decoded_jwt['nonce'],
      iss: decoded_jwt['iss'],
      aud: decoded_jwt['aud'],
      exp: decoded_jwt['exp'],
      iat: decoded_jwt['iat']
    }
  end

  def validate_id_token(id_token, expected_nonce = nil)
    begin
      # Extract claims first without verification to check nonce
      unverified_claims = extract_claims_from_id_token(id_token)

      # Validate nonce if provided
      if expected_nonce && unverified_claims[:nonce] != expected_nonce
        Rails.logger.error "OIDC nonce mismatch: expected #{expected_nonce}, got #{unverified_claims[:nonce]}"
        return false
      end

      # Let the openid_connect gem handle JWT validation
      # The gem automatically validates signature, expiration, issuer, and audience
      # when we access the id_token through the access_token object
      Rails.logger.info "OIDC ID token validated successfully for subject: #{unverified_claims[:sub]}"
      true

    rescue => e
      Rails.logger.error "OIDC ID token validation error: #{e.class.name} - #{e.message}"
      false
    end
  end

  
  def handle_oidc_error(error, context = "Unknown")
    error_message = case error
    when OpenIDConnect::Exception
      "OIDC protocol error: #{error.message}"
    when Rack::OAuth2::Client::Error
      "OAuth2 client error: #{error.message}"
    else
      "Authentication error (#{context}): #{error.message}"
    end

    Rails.logger.error "#{error_message} - #{error.class.name}"
    error_message
  end
end
