/*
 * Copyright 2025 Hirokazu Kobayashi
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.idp.server.core.openid.oauth.handler;

import java.util.HashMap;
import java.util.Map;
import org.idp.server.core.openid.grant_management.AuthorizationGranted;
import org.idp.server.core.openid.grant_management.AuthorizationGrantedRepository;
import org.idp.server.core.openid.oauth.*;
import org.idp.server.core.openid.oauth.clientauthenticator.ClientAuthenticationHandler;
import org.idp.server.core.openid.oauth.clientauthenticator.clientcredentials.ClientCredentials;
import org.idp.server.core.openid.oauth.configuration.AuthorizationServerConfiguration;
import org.idp.server.core.openid.oauth.configuration.AuthorizationServerConfigurationQueryRepository;
import org.idp.server.core.openid.oauth.configuration.client.ClientConfiguration;
import org.idp.server.core.openid.oauth.configuration.client.ClientConfigurationQueryRepository;
import org.idp.server.core.openid.oauth.context.OAuthRequestContextCreator;
import org.idp.server.core.openid.oauth.context.OAuthRequestContextCreators;
import org.idp.server.core.openid.oauth.dpop.DPoPHeaderValidator;
import org.idp.server.core.openid.oauth.dpop.DPoPProof;
import org.idp.server.core.openid.oauth.dpop.DPoPProofVerifiedResult;
import org.idp.server.core.openid.oauth.dpop.DPoPProofVerifier;
import org.idp.server.core.openid.oauth.exception.OAuthBadRequestException;
import org.idp.server.core.openid.oauth.factory.RequestObjectFactories;
import org.idp.server.core.openid.oauth.gateway.RequestObjectGateway;
import org.idp.server.core.openid.oauth.io.OAuthPushedRequest;
import org.idp.server.core.openid.oauth.io.OAuthRequest;
import org.idp.server.core.openid.oauth.repository.AuthorizationRequestRepository;
import org.idp.server.core.openid.oauth.request.OAuthRequestParameters;
import org.idp.server.core.openid.oauth.type.oauth.RequestedClientId;
import org.idp.server.core.openid.oauth.validator.OAuthPushedRequestValidator;
import org.idp.server.core.openid.oauth.validator.OAuthRequestValidator;
import org.idp.server.core.openid.oauth.verifier.OAuthRequestVerifier;
import org.idp.server.core.openid.session.OPSession;
import org.idp.server.platform.log.LoggerWrapper;
import org.idp.server.platform.multi_tenancy.tenant.Tenant;

/** OAuthRequestHandler */
public class OAuthRequestHandler {

  LoggerWrapper log = LoggerWrapper.getLogger(OAuthRequestHandler.class);
  OAuthRequestContextCreators oAuthRequestContextCreators;
  OAuthRequestVerifier verifier;
  ClientAuthenticationHandler clientAuthenticationHandler;
  AuthorizationRequestRepository authorizationRequestRepository;
  AuthorizationServerConfigurationQueryRepository authorizationServerConfigurationQueryRepository;
  ClientConfigurationQueryRepository clientConfigurationQueryRepository;
  AuthorizationGrantedRepository grantedRepository;

  public OAuthRequestHandler(
      AuthorizationRequestRepository authorizationRequestRepository,
      AuthorizationServerConfigurationQueryRepository
          authorizationServerConfigurationQueryRepository,
      ClientConfigurationQueryRepository clientConfigurationQueryRepository,
      RequestObjectGateway requestObjectGateway,
      RequestObjectFactories requestObjectFactories,
      AuthorizationGrantedRepository grantedRepository) {
    this.oAuthRequestContextCreators =
        new OAuthRequestContextCreators(
            requestObjectGateway, authorizationRequestRepository, requestObjectFactories);
    this.verifier = new OAuthRequestVerifier();
    this.clientAuthenticationHandler = new ClientAuthenticationHandler();
    this.authorizationRequestRepository = authorizationRequestRepository;
    this.authorizationServerConfigurationQueryRepository =
        authorizationServerConfigurationQueryRepository;
    this.clientConfigurationQueryRepository = clientConfigurationQueryRepository;
    this.grantedRepository = grantedRepository;
  }

  /**
   * Handles a pushed authorization request (PAR).
   *
   * <p>Per RFC 9126 Section 2.1, the PAR endpoint applies the same client authentication rules as
   * the token endpoint. Per RFC 7521 Section 4.2, client_id is resolved with fallback: explicit
   * parameter → Basic Auth → client_assertion JWT iss claim.
   */
  public OAuthPushedRequestContext handlePushedRequest(OAuthPushedRequest pushedRequest) {
    OAuthRequestParameters requestParameters = pushedRequest.toOAuthRequestParameters();
    Tenant tenant = pushedRequest.tenant();
    RequestedClientId clientId = pushedRequest.clientId();

    log.trace(
        "OAuth pushed request started: client={}, response_type={}",
        clientId.value(),
        requestParameters.responseType().value());

    OAuthPushedRequestValidator validator = new OAuthPushedRequestValidator(tenant, pushedRequest);
    validator.validate();
    new DPoPHeaderValidator(pushedRequest.dpopProofHeaders()).validate();

    AuthorizationServerConfiguration authorizationServerConfiguration =
        authorizationServerConfigurationQueryRepository.get(tenant);
    ClientConfiguration clientConfiguration =
        clientConfigurationQueryRepository.get(tenant, clientId);

    // RFC 9449 §10.1: PAR + DPoP integration
    // If a DPoP proof is presented at the PAR endpoint, verify it and bind the resulting JWK
    // thumbprint to the pushed authorization request as dpop_jkt.
    OAuthRequestParameters effectiveParameters =
        applyDPoPProofToParameters(
            pushedRequest, requestParameters, tenant, authorizationServerConfiguration);

    OAuthRequestPattern oAuthRequestPattern = effectiveParameters.analyzePattern();
    OAuthRequestContextCreator oAuthRequestContextCreator =
        oAuthRequestContextCreators.get(oAuthRequestPattern);

    OAuthRequestContext context =
        oAuthRequestContextCreator.create(
            tenant,
            effectiveParameters,
            authorizationServerConfiguration,
            clientConfiguration,
            true);

    OAuthPushedRequestContext oAuthPushedRequestContext =
        new OAuthPushedRequestContext(
            context,
            pushedRequest.clientSecretBasic(),
            pushedRequest.toClientCert(),
            pushedRequest.toBackchannelParameters());
    ClientCredentials clientCredentials =
        clientAuthenticationHandler.authenticate(oAuthPushedRequestContext);

    verifier.verify(context, clientCredentials);

    authorizationRequestRepository.register(tenant, context.authorizationRequest());

    log.info(
        "OAuth pushed request completed: client={}, request_uri={}",
        clientId.value(),
        context.authorizationRequest().identifier().value());

    return oAuthPushedRequestContext;
  }

  public OAuthRequestContext handleRequest(OAuthRequest oAuthRequest) {
    OAuthRequestParameters parameters = oAuthRequest.toParameters();
    Tenant tenant = oAuthRequest.tenant();

    log.trace(
        "OAuth request started: client={}, response_type={}",
        parameters.clientId().value(),
        parameters.responseType().value());
    OAuthRequestValidator validator = new OAuthRequestValidator(tenant, parameters);
    validator.validate();

    AuthorizationServerConfiguration authorizationServerConfiguration =
        authorizationServerConfigurationQueryRepository.get(tenant);
    ClientConfiguration clientConfiguration =
        clientConfigurationQueryRepository.get(tenant, parameters.clientId());

    OAuthRequestPattern oAuthRequestPattern = parameters.analyzePattern();
    OAuthRequestContextCreator oAuthRequestContextCreator =
        oAuthRequestContextCreators.get(oAuthRequestPattern);

    OAuthRequestContext context =
        oAuthRequestContextCreator.create(
            tenant, parameters, authorizationServerConfiguration, clientConfiguration, false);
    verifier.verify(context);

    if (!context.isPushedRequest()) {
      authorizationRequestRepository.register(tenant, context.authorizationRequest());
    }

    // Set OPSession for prompt=none handling
    OPSession opSession = oAuthRequest.opSession();
    if (opSession != null && opSession.exists() && opSession.isActive()) {
      context.setOPSession(opSession);

      AuthorizationGranted authorizationGranted =
          grantedRepository.find(tenant, parameters.clientId(), opSession.user());
      context.setAuthorizationGranted(authorizationGranted);
    }

    return context;
  }

  /**
   * RFC 9449 §10.1: PAR + DPoP integration.
   *
   * <p>If a DPoP proof is presented at the PAR endpoint:
   *
   * <ul>
   *   <li>verify the proof JWT
   *   <li>if a {@code dpop_jkt} parameter is also present, the two JWK thumbprints MUST match
   *   <li>otherwise, override {@code dpop_jkt} with the thumbprint extracted from the proof so that
   *       the issued request_uri is bound to that key
   * </ul>
   *
   * <p>If no DPoP proof is presented, the parameters pass through unchanged (the {@code dpop_jkt}
   * parameter, if any, has already been validated upstream by RFC 9449 §10).
   */
  private OAuthRequestParameters applyDPoPProofToParameters(
      OAuthPushedRequest pushedRequest,
      OAuthRequestParameters requestParameters,
      Tenant tenant,
      AuthorizationServerConfiguration authorizationServerConfiguration) {
    if (pushedRequest.dpopProofHeaders().isEmpty()) {
      return requestParameters;
    }
    DPoPProof dpopProof = new DPoPProof(pushedRequest.dpopProofHeaders().get(0));
    DPoPProofVerifiedResult dpopResult =
        new DPoPProofVerifier()
            .verifyIfNeeded(
                dpopProof,
                pushedRequest.httpMethod(),
                pushedRequest.httpUri(),
                authorizationServerConfiguration.dpopSigningAlgValuesSupported());
    if (!dpopResult.exists()) {
      return requestParameters;
    }
    String proofJkt = dpopResult.jwkThumbprint().value();

    if (requestParameters.hasDPoPJkt() && !requestParameters.dpopJkt().value().equals(proofJkt)) {
      throw new OAuthBadRequestException(
          "invalid_request",
          "dpop_jkt parameter does not match the JWK thumbprint of the DPoP proof",
          tenant);
    }

    Map<String, String[]> overridden = new HashMap<>(pushedRequest.getParams());
    overridden.put("dpop_jkt", new String[] {proofJkt});
    return new OAuthRequestParameters(overridden);
  }
}
