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

import com.webauthn4j.metadata.data.statement.MetadataStatement;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.idp.server.authentication.interactors.fido2.Fido2ExecutorType;
import org.idp.server.authenticators.webauthn4j.mds.MdsResolver;
import org.idp.server.core.openid.authentication.AuthenticationTransactionIdentifier;
import org.idp.server.core.openid.authentication.config.AuthenticationExecutionConfig;
import org.idp.server.core.openid.authentication.interaction.execution.AuthenticationExecutionRequest;
import org.idp.server.core.openid.authentication.interaction.execution.AuthenticationExecutionResult;
import org.idp.server.core.openid.authentication.interaction.execution.AuthenticationExecutor;
import org.idp.server.core.openid.authentication.repository.AuthenticationInteractionCommandRepository;
import org.idp.server.core.openid.authentication.repository.AuthenticationInteractionQueryRepository;
import org.idp.server.platform.json.JsonConverter;
import org.idp.server.platform.log.LoggerWrapper;
import org.idp.server.platform.multi_tenancy.tenant.Tenant;
import org.idp.server.platform.type.RequestAttributes;

public class WebAuthn4jRegistrationExecutor implements AuthenticationExecutor {

  LoggerWrapper log = LoggerWrapper.getLogger(WebAuthn4jRegistrationExecutor.class);

  AuthenticationInteractionCommandRepository transactionCommandRepository;
  AuthenticationInteractionQueryRepository transactionQueryRepository;
  WebAuthn4jCredentialRepository credentialRepository;
  MdsResolver mdsResolver;
  JsonConverter jsonConverter;

  public WebAuthn4jRegistrationExecutor(
      AuthenticationInteractionCommandRepository transactionCommandRepository,
      AuthenticationInteractionQueryRepository transactionQueryRepository,
      WebAuthn4jCredentialRepository credentialRepository) {
    this(transactionCommandRepository, transactionQueryRepository, credentialRepository, null);
  }

  public WebAuthn4jRegistrationExecutor(
      AuthenticationInteractionCommandRepository transactionCommandRepository,
      AuthenticationInteractionQueryRepository transactionQueryRepository,
      WebAuthn4jCredentialRepository credentialRepository,
      MdsResolver mdsResolver) {
    this.transactionCommandRepository = transactionCommandRepository;
    this.transactionQueryRepository = transactionQueryRepository;
    this.credentialRepository = credentialRepository;
    this.mdsResolver = mdsResolver;
    this.jsonConverter = JsonConverter.snakeCaseInstance();
  }

  public Fido2ExecutorType type() {
    return new Fido2ExecutorType("webauthn4j");
  }

  @Override
  public String function() {
    return "webauthn4j_registration";
  }

  @Override
  public AuthenticationExecutionResult execute(
      Tenant tenant,
      AuthenticationTransactionIdentifier identifier,
      AuthenticationExecutionRequest request,
      RequestAttributes requestAttributes,
      AuthenticationExecutionConfig configuration) {

    try {
      log.debug("webauthn4j registration, retrieving challenge context from transaction");

      WebAuthn4jChallengeContext context =
          transactionQueryRepository.get(
              tenant, identifier, type().value(), WebAuthn4jChallengeContext.class);

      WebAuthn4jChallenge webAuthn4jChallenge = context.challenge();
      WebAuthn4jUser expectedUser = context.user();

      log.debug(
          "webauthn4j registration, retrieved challenge and user info for: {}",
          expectedUser.name());

      // Validate user ID from request matches the expected user ID from challenge context
      String receivedUserId = request.optValueAsString("userId", null);
      if (receivedUserId != null && !expectedUser.id().equals(receivedUserId)) {
        log.error(
            "webauthn4j registration failed: user ID mismatch. Expected: {}, Received: {}",
            expectedUser.id(),
            receivedUserId);
        throw new WebAuthn4jBadRequestException(
            "User ID mismatch. The credential was created for a different user.");
      }

      String requestString = jsonConverter.write(request.toMap());
      WebAuthn4jConfiguration webAuthn4jConfiguration =
          jsonConverter.read(configuration.details(), WebAuthn4jConfiguration.class);

      log.debug(
          "webauthn4j registration, verifying credential for user: {}, displayName: {}",
          expectedUser.name(),
          expectedUser.displayName());

      WebAuthn4jRegistrationManager manager =
          new WebAuthn4jRegistrationManager(
              webAuthn4jConfiguration,
              webAuthn4jChallenge,
              requestString,
              expectedUser.id(),
              expectedUser.name(),
              expectedUser.displayName());

      WebAuthn4jCredential webAuthn4jCredential = manager.verifyAndCreateCredential();

      log.debug(
          "webauthn4j registration, credential created with id: {}", webAuthn4jCredential.id());

      // Enrich credential with MDS metadata
      enrichWithMdsMetadata(webAuthn4jCredential);

      credentialRepository.register(tenant, webAuthn4jCredential);

      Map<String, Object> response = new HashMap<>();
      response.put("execution_webauthn4j", webAuthn4jCredential.toMap());

      log.info(
          "webauthn4j registration succeeded. credential id: {}, userId: {}, username: {}",
          webAuthn4jCredential.id(),
          expectedUser.id(),
          expectedUser.name());
      return AuthenticationExecutionResult.success(response);

    } catch (WebAuthn4jBadRequestException e) {
      log.error("webauthn4j registration failed: {}", e.getMessage(), e);
      Map<String, Object> errorResponse = new HashMap<>();
      errorResponse.put("error", "registration_failed");
      errorResponse.put("error_description", e.getMessage());
      return AuthenticationExecutionResult.clientError(errorResponse);

    } catch (Exception e) {
      log.error("webauthn4j unexpected error during registration", e);
      Map<String, Object> errorResponse = new HashMap<>();
      errorResponse.put("error", "server_error");
      errorResponse.put("error_description", "An unexpected error occurred during registration");
      return AuthenticationExecutionResult.serverError(errorResponse);
    }
  }

  /**
   * Enriches credential metadata with FIDO MDS information.
   *
   * <p>Fetches authenticator metadata from FIDO Metadata Service using the AAGUID and stores
   * relevant information in the credential's metadata field.
   */
  private void enrichWithMdsMetadata(WebAuthn4jCredential credential) {
    if (mdsResolver == null) {
      log.debug("MDS resolver not configured, skipping metadata enrichment");
      return;
    }

    String aaguid = credential.aaguid();
    if (aaguid == null || aaguid.isEmpty()) {
      log.debug("No AAGUID available, skipping MDS metadata enrichment");
      return;
    }

    try {
      Optional<MetadataStatement> metadataOpt = mdsResolver.resolve(aaguid);
      Map<String, Object> metadata = credential.metadata();

      if (metadataOpt.isPresent()) {
        MetadataStatement statement = metadataOpt.get();

        // Store authenticator information from MDS
        metadata.put("authenticator_name", statement.getDescription());
        if (statement.getIcon() != null) {
          metadata.put("authenticator_icon", statement.getIcon());
        }

        log.info(
            "MDS metadata enriched for AAGUID {}: name={}", aaguid, statement.getDescription());
      } else {
        log.debug("No MDS metadata found for AAGUID: {}", aaguid);
      }

      // Check authenticator status (compromised, revoked, etc.)
      boolean isCompromised = mdsResolver.isCompromised(aaguid);
      metadata.put("is_compromised", isCompromised);
      if (isCompromised) {
        log.warn("Authenticator with AAGUID {} is marked as COMPROMISED in MDS", aaguid);
      }

      metadata.put("mds_fetched_at", System.currentTimeMillis());

    } catch (Exception e) {
      log.warn("Failed to fetch MDS metadata for AAGUID {}: {}", aaguid, e.getMessage());
      // Don't fail registration if MDS lookup fails
    }
  }
}
