/*
 * 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.verifier;

import org.idp.server.core.openid.oauth.AuthorizationProfile;
import org.idp.server.core.openid.oauth.OAuthRequestContext;
import org.idp.server.core.openid.oauth.exception.OAuthBadRequestException;
import org.idp.server.core.openid.oauth.type.extension.RegisteredRedirectUris;
import org.idp.server.core.openid.oauth.verifier.base.OAuthRequestBaseVerifier;
import org.idp.server.platform.http.InvalidUriException;
import org.idp.server.platform.http.UriMatcher;
import org.idp.server.platform.http.UriWrapper;

public class OAuth2RequestVerifier implements AuthorizationRequestVerifier {

  OAuthRequestBaseVerifier baseVerifier = new OAuthRequestBaseVerifier();

  public AuthorizationProfile profile() {
    return AuthorizationProfile.OAUTH2;
  }

  @Override
  public void verify(OAuthRequestContext context) {
    throwExceptionIfInvalidRedirectUri(context);
    baseVerifier.verify(context);
  }

  /**
   * 3.1.2.4. Invalid Endpoint
   *
   * <p>If an authorization request fails validation due to a missing, invalid, or mismatching
   * redirection URI, the authorization server SHOULD inform the resource owner of the error and
   * MUST NOT automatically redirect the user-agent to the invalid redirection URI.
   *
   * @param context
   * @see <a href="https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.4">3.1.2.4. Invalid
   *     Endpoint</a>
   */
  void throwExceptionIfInvalidRedirectUri(OAuthRequestContext context) {
    if (context.hasRedirectUriInRequest()) {
      throwExceptionIfNotAbsoluteRedirectUri(context);
      throwExceptionIfRedirectUriContainsFragment(context);
      throwExceptionIfUnMatchRedirectUri(context);
    } else {
      throwExceptionIfMultiRegisteredRedirectUri(context);
    }
  }

  /**
   * RFC 6749 Section 3.1.2: redirect_uri MUST be absolute URI
   *
   * <p>The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.
   *
   * @param context
   * @see <a href="https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2">3.1.2. Redirection
   *     Endpoint</a>
   */
  void throwExceptionIfNotAbsoluteRedirectUri(OAuthRequestContext context) {
    if (!UriMatcher.isAbsoluteUri(context.redirectUri().value())) {
      throw new OAuthBadRequestException(
          "invalid_request",
          String.format("redirect_uri must be an absolute URI (%s)", context.redirectUri().value()),
          context.tenant());
    }
  }

  /**
   * The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3. The
   * endpoint URI MAY include an "application/x-www-form-urlencoded" formatted (per Appendix B)
   * query component ([RFC3986] Section 3.4), which MUST be retained when adding additional query
   * parameters. The endpoint URI MUST NOT include a fragment component.
   *
   * @param context
   * @see <a href="https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2">3.1.2. Redirection
   *     Endpoint</a>
   */
  void throwExceptionIfRedirectUriContainsFragment(OAuthRequestContext context) {
    try {
      UriWrapper uri = new UriWrapper(context.redirectUri().value());
      if (uri.hasFragment()) {
        throw new OAuthBadRequestException(
            "invalid_request",
            String.format("redirect_uri must not fragment (%s)", context.redirectUri().value()),
            context.tenant());
      }
    } catch (InvalidUriException exception) {
      throw new OAuthBadRequestException(
          "invalid_request",
          String.format(
              "authorization request redirect_uri is invalid (%s)", context.redirectUri().value()),
          context.tenant());
    }
  }

  /**
   * 3.1.2.3. Dynamic Configuration
   *
   * <p>If multiple redirection URIs have been registered, if only part of the redirection URI has
   * been registered, or if no redirection URI has been registered, the client MUST include a
   * redirection URI with the authorization request using the "redirect_uri" request parameter.
   *
   * <p>When a redirection URI is included in an authorization request, the authorization server
   * MUST compare and match the value received against at least one of the registered redirection
   * URIs (or URI components) as defined in [RFC3986] Section 6, if any redirection URIs were
   * registered. If the client registration included the full redirection URI, the authorization
   * server MUST compare the two URIs using simple string comparison as defined in [RFC3986] Section
   * 6.2.1.
   *
   * <p>RFC 8252 Section 7.3: For native apps using loopback redirect URIs (localhost/127.0.0.1),
   * the port can be dynamic and should be ignored during comparison.
   *
   * @param context
   * @see <a href="https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3">3.1.2.3. Dynamic
   *     Configuration</a>
   * @see <a href="https://www.rfc-editor.org/rfc/rfc8252#section-7.3">RFC 8252 Section 7.3</a>
   * @see <a href="https://www.rfc-editor.org/rfc/rfc3986.html">rfc3986</a>
   */
  void throwExceptionIfUnMatchRedirectUri(OAuthRequestContext context) {
    RegisteredRedirectUris registeredRedirectUris = context.registeredRedirectUris();
    String redirectUri = context.redirectUri().value();

    // RFC 8252 Section 7.3: For native apps, loopback redirect URIs can use dynamic ports
    if (context.isNativeApplication() && UriMatcher.isLoopbackUri(redirectUri)) {
      if (!registeredRedirectUris.containsWithLoopbackPortAllowance(redirectUri)) {
        throw new OAuthBadRequestException(
            "invalid_request",
            String.format(
                "authorization request redirect_uri does not match registered redirect uris (%s)",
                redirectUri),
            context.tenant());
      }
      return;
    }

    // Standard comparison with syntax-based normalization
    if (!registeredRedirectUris.containsWithNormalizationAndComparison(redirectUri)) {
      throw new OAuthBadRequestException(
          "invalid_request",
          String.format(
              "redirect_uri does not match any registered redirect_uri, simple string comparison failed (%s)",
              redirectUri),
          context.tenant());
    }
    // SIMPLE_MATCH and NORMALIZED_MATCH are both allowed
  }

  /**
   * 3.1.2.3. Dynamic Configuration
   *
   * <p>If multiple redirection URIs have been registered, if only part of the redirection URI has
   * been registered, or if no redirection URI has been registered, the client MUST include a
   * redirection URI with the authorization request using the "redirect_uri" request parameter.
   *
   * @param context
   * @see <a href="https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3">3.1.2.3. Dynamic
   *     Configuration</a>
   */
  void throwExceptionIfMultiRegisteredRedirectUri(OAuthRequestContext context) {
    if (context.isMultiRegisteredRedirectUri()) {
      throw new OAuthBadRequestException(
          "invalid_request",
          "on multiple registered redirect uris, authorization request redirect_uri must contains",
          context.tenant());
    }
  }
}
