package com.airlock.iam.admin.infrastructure.resource.prot.user;

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.data.ResourceObject.Builder;
import ch.ergon.restal.jsonapi.document.request.RequestDocument;
import ch.ergon.restal.jsonapi.parameter.FilterParameter;
import ch.ergon.restal.jsonapi.parameter.PageParameter;
import ch.ergon.restal.jsonapi.parameter.Pager;
import ch.ergon.restal.jsonapi.parameter.SortParameter;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Condition;
import ch.ergon.restal.jsonapi.parameter.SortParameter.Criterion;
import com.airlock.iam.admin.application.service.authmethod.AuthenticationMethodService;
import com.airlock.iam.admin.application.service.user.ContextDataAdminService;
import com.airlock.iam.admin.application.service.user.UserAdminService;
import com.airlock.iam.admin.application.service.user.UsersExportService;
import com.airlock.iam.admin.application.service.user.UsersExporter;
import com.airlock.iam.admin.domain.model.user.UserSpecificSettings;
import com.airlock.iam.admin.infrastructure.error.user.UserErrorCodeMapper;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.UserDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.UserDataResourceDocument;
import com.airlock.iam.admin.infrastructure.mapper.prot.ContextDataDateConverter;
import com.airlock.iam.admin.infrastructure.transfer.request.user.UserCreateRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.user.UserData;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.infrastructure.validation.DocumentedFilterParameter;
import com.airlock.iam.base.infrastructure.validation.DocumentedSortParameter;
import com.airlock.iam.base.infrastructure.validation.SanitizedPageParameter;
import com.airlock.iam.base.infrastructure.validation.ValidPageParameter;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.api.infrastructure.annotation.DisableCsrfFilter;
import com.airlock.iam.common.application.query.QueryParameters;
import com.airlock.iam.common.application.query.QuerySettings;
import com.airlock.iam.common.application.query.UserStoreQuery;
import com.airlock.iam.common.domain.model.password.PasswordImpl;
import com.airlock.iam.common.infrastructure.mapper.OrikaMapperUtils;
import com.airlock.iam.common.infrastructure.resource.Resources;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.Response.Status;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MappingContext;
import org.apache.commons.lang3.StringUtils;

@Path("/users")
public class UsersResource {
   private static final String USER_CREATE_REQ_EXAMPLE = "{\n\t\"data\": {\n\t  \"attributes\": {},\n\t  \"relationships\": {},\n\t  \"type\": \"user.creation\",\n\t  \"id\": \"itester\"\n\t}\n}\n";
   private static final String CASE_SENSITIVE = "caseSensitive";
   private static final String EXPORT = "export";
   private static final String USER_CREATE_REQ_EXAMPLE_NAME = "Create user request";
   private final Provider<ExternalUriInfo> uriInfo;
   private final Provider<UserStore> userStore;
   private final Provider<Optional<UserStore>> userListDetailLoader;
   private final Provider<UserAdminService> userAdminService;
   private final HelpdeskLogger helpdeskLogger;
   private final MapperFacade mapper;
   private final Provider<QuerySettings> querySettings;
   private final Provider<ContextDataAdminService> contextDataService;
   private final Provider<UsersExportService> usersExportService;
   private final Provider<UserSpecificSettings> userSettings;
   private final Provider<AuthenticationMethodService> authMethodService;
   private final Provider<ContextDataDateConverter> dateConverter;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public UsersResource(
      Provider<ExternalUriInfo> uriInfo,
      @Named("MANAGED_USERS_STORE") Provider<UserStore> userStore,
      @Named("USER_LIST_DETAIL_LOADER") Provider<Optional<UserStore>> userListDetailLoader,
      Provider<UserAdminService> userAdminService,
      HelpdeskLogger helpdeskLogger,
      MapperFacade mapper,
      Provider<QuerySettings> querySettings,
      Provider<ContextDataAdminService> contextDataService,
      Provider<UsersExportService> usersExportService,
      Provider<UserSpecificSettings> settings,
      Provider<AuthenticationMethodService> authMethodService,
      Provider<ContextDataDateConverter> dateConverter,
      ValidationToErrorMapper errorMapper
   ) {
      this.uriInfo = uriInfo;
      this.userStore = userStore;
      this.userListDetailLoader = userListDetailLoader;
      this.userAdminService = userAdminService;
      this.helpdeskLogger = helpdeskLogger;
      this.mapper = mapper;
      this.querySettings = querySettings;
      this.contextDataService = contextDataService;
      this.usersExportService = usersExportService;
      this.userSettings = settings;
      this.authMethodService = authMethodService;
      this.dateConverter = dateConverter;
      this.errorMapper = errorMapper;
   }

   private static List<String> collectContextDataFilterColumns(FilterParameter filterParam) {
      return filterParam.parse()
         .stream()
         .flatMap(disjunction -> disjunction.getConditions().stream())
         .<String>map(Condition::getProperty)
         .filter(column -> !QueryParameters.getReservedParameters().contains(column))
         .collect(Collectors.toList());
   }

   private static List<String> collectContextDataSortColumns(SortParameter sortParameter) {
      return sortParameter.parse()
         .stream()
         .<String>map(Criterion::getName)
         .filter(column -> !QueryParameters.getReservedParameters().contains(column))
         .collect(Collectors.toList());
   }

   @ApiResponse(
      responseCode = "200",
      description = "The response contains a page of users.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = UserDataCollectionDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = UserDataCollectionDocument.class)),
            @Content(mediaType = "application/csv", schema = @Schema(type = "string"))
      }
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Reads a page with users.\n<ul>\n<li><b>filter</b>: Restricts the set of users to be returned. See <a href=\"#filtering\">Filtering</a>. Filter\nnames are mapped to the names of user context data items. Only string-valued items are applicable. Note that this\nalso includes list user profile items.<br>\nAdditionally, the following built-in filter names are available:\n<ul>\n<li><tt>username</tt>: Filters the user's identity.</li>\n<li><tt>locked</tt>: Filters the lock state. Either <tt>false</tt> or <tt>true</tt>.</li>\n<li><tt>last-login-attempt</tt>: Filters the time of the latest authentication attempt by a period.</li>\n<li><tt>last-successful-login</tt>: Filters the time of the latest successful authentication by a period.</li>\n</ul>\nPeriods are specified with one of the following values: <tt>last_day</tt>, <tt>last_week</tt>, <tt>last_month</tt> and <tt>last_year</tt>.\n</li>\n</ul>\n<div class=\"iam-resource-return\">A collection document containing the requested page with users.</div>",
      tags = "Users",
      summary = "Read user page"
   )
   @DisableCsrfFilter(UsersResource.HasExportParameter.class)
   public Response retrieve(
      @Parameter(description = "Restricts the set of all users to those on the requested page.") @ValidPageParameter @BeanParam SanitizedPageParameter sanitizedPageParameter,
      @Parameter(description = "Restricts the set of all users to those matching the filter condition.") @BeanParam DocumentedFilterParameter filterParameter,
      @Parameter(description = "Sorts the collected set of users.") @BeanParam DocumentedSortParameter sortParameter,
      @Parameter(description = "(optional) whether the search is case sensitive or not. If not set, the configuration's default value is used.") @QueryParam(
   "caseSensitive"
) Boolean caseSensitive,
      @Parameter(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true"))) @QueryParam("export") String export
   ) {
      if (StringUtils.isNotBlank(export)) {
         return this.export(sanitizedPageParameter.originallyRequestedPaging(), filterParameter, sortParameter, caseSensitive, export);
      }

      Optional<Response> errorResponse = this.validationErrorResponse(filterParameter, sortParameter);
      if (errorResponse.isPresent()) {
         return errorResponse.get();
      }

      UserStoreQuery query = this.createQueryForValidatedParameters(sanitizedPageParameter, filterParameter, sortParameter, caseSensitive);
      CollectionDocument<UserData> document = new CollectionDocument();
      UriBuilder uri = Resources.uri((ExternalUriInfo)this.uriInfo.get(), UsersResource.class)
         .replaceQuery(((ExternalUriInfo)this.uriInfo.get()).getRequestUri().getQuery());
      this.removeParametersWithName(uri, "page[limit]", "page[offset]");
      Pager pager = new Pager(uri);
      document.setLinks(pager.createPagingLinks(sanitizedPageParameter.getOffset(), sanitizedPageParameter.getSize(), query.getTotalCount()));
      UriBuilder userSelfUriBuilder = Resources.uri((ExternalUriInfo)this.uriInfo.get(), UserResource.class);

      try (Stream<PersistentUser> users = query.getUsers()) {
         users.map(user -> {
            String userId = user.getName();
            return ResourceObject.builder(userId, this.toUserData(user)).linkSelf(userSelfUriBuilder.build(new Object[]{userId})).build();
         }).forEach(document::add);
      }

      return Response.ok(document).build();
   }

   @ApiResponse(responseCode = "200", description = "The response contains the users as CSV.", content = @Content(mediaType = "application/csv"))
   @GET
   @Operation(
      description = "Exports users to CSV.\n<div class=\"iam-resource-return\">The users as CSV containing the information configured in user data download section.</div>",
      extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")),
      summary = "Export users to CSV"
   )
   @Produces("application/csv;qs=0.8")
   @DisableCsrfFilter(UsersResource.HasExportParameter.class)
   public Response export(
      @Parameter(description = "Restricts the set of all users to those on the requested page.") @BeanParam PageParameter pageParameter,
      @Parameter(description = "Restricts the set of all users to those matching the filter condition.") @BeanParam DocumentedFilterParameter filterParameter,
      @Parameter(description = "Sorts the collected set of users.") @BeanParam DocumentedSortParameter sortParameter,
      @Parameter(description = "(optional) whether the search is case sensitive or not. If not set, the configuration's default value is used.") @QueryParam(
   "caseSensitive"
) Boolean caseSensitive,
      @QueryParam("export") String export
   ) {
      Optional<Response> errorResponse = this.validationErrorResponse(filterParameter, sortParameter);
      if (errorResponse.isPresent()) {
         return errorResponse.get();
      }

      Optional<UsersExporter> exporter = ((UsersExportService)this.usersExportService.get()).getExporter(export);
      if (exporter.isEmpty()) {
         return ErrorResponse.badRequest().build().toResponse();
      }

      UserStoreQuery query = this.createQueryForValidatedParameters(pageParameter, filterParameter, sortParameter, caseSensitive);

      try (Stream<PersistentUser> users = query.getUsers()) {
         UsersExporter.UsersExportResult result = exporter.get().exportUsers(users);
         return Response.ok()
            .header("Content-Type", result.contentType())
            .header("Content-Disposition", "attachment; filename=" + result.fileName())
            .header("Pragma", "")
            .header("Cache-Control", "")
            .entity(result.data())
            .build();
      }
   }

   private Optional<Response> validationErrorResponse(FilterParameter filterParameter, SortParameter sortParameter) {
      ValidationResult filterValidation = ((ContextDataAdminService)this.contextDataService.get())
         .validateContextDataFilterNames(collectContextDataFilterColumns(filterParameter));
      if (!filterValidation.isValid()) {
         return Optional.of(this.mapToValidationErrorResponse(filterValidation, "filter"));
      }

      ValidationResult sortValidation = ((ContextDataAdminService)this.contextDataService.get())
         .validateSortableContextDataNames(collectContextDataSortColumns(sortParameter));
      return !sortValidation.isValid() ? Optional.of(this.mapToValidationErrorResponse(sortValidation, "sort")) : Optional.empty();
   }

   private Response mapToValidationErrorResponse(ValidationResult validationResult, String sourceParameter) {
      return this.errorMapper.withSourceParameter(sourceParameter).mapToResponse(validationResult.getErrors());
   }

   private UserData toUserData(PersistentUser persistentUser) {
      Map<String, Object> processedContextData = ((ContextDataAdminService)this.contextDataService.get())
         .filterAndTransformContextData(persistentUser.getContextData());
      MappingContext mappingContext = OrikaMapperUtils.initMappingContext();
      mappingContext.setProperty(
         "additionalActiveAuthMethodKeys", ((AuthenticationMethodService)this.authMethodService.get()).getAdditionalActiveAuthMethodKeys()
      );
      UserData data = (UserData)this.mapper.map(persistentUser, UserData.class, mappingContext);

      for (Entry<String, Object> entry : processedContextData.entrySet()) {
         data.addContextData(entry.getKey(), entry.getValue());
      }

      return data;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "201",
               description = "A new user was created.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = UserDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = UserDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>PASSWORD_POLICY_VIOLATED</td>\n<td>The password violates the password policy. See <a href=\"#passwordPolicyDetailCodes\">password policy violation detail codes</a>.</td>\n</tr>\n</table>\n</div>\n",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
               }
            ),
            @ApiResponse(
               responseCode = "403",
               description = "The user is not editable.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
               }
            ),
            @ApiResponse(
               responseCode = "409",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>USER_NOT_UNIQUE</td>\n<td>The supplied username already exists.</td>\n</tr>\n</table>\n</div>\n",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
               }
            )
      }
   )
   @POST
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Creates a new user. The client may choose the username via JSON API's 'data.id' attribute.\nIf no 'data.id' is provided, the server generates a unique username.\n<div class=\"iam-resource-return\">A response containing the type and id attribute of the created user.\n The response contains a Location header referring to the created user.</div>",
      tags = "Users",
      summary = "Create new user"
   )
   public Response create(
      @NotNull @Valid @RequestBody(
   description = "A request object with the desired username (data.id), password and context data. Context data attributes can be put directly into the 'data.attributes' element.\nThe active configuration defines via profile items for 'Rows on User Detail Page', which context data attributes are mandatory.",
   content = {
         @Content(
            mediaType = "application/vnd.api+json",
            examples = @ExampleObject(
               name = "Create user request",
               value = "{\n\t\"data\": {\n\t  \"attributes\": {},\n\t  \"relationships\": {},\n\t  \"type\": \"user.creation\",\n\t  \"id\": \"itester\"\n\t}\n}\n"
            )
         ),
         @Content(
            mediaType = "application/json",
            examples = @ExampleObject(
               name = "Create user request",
               value = "{\n\t\"data\": {\n\t  \"attributes\": {},\n\t  \"relationships\": {},\n\t  \"type\": \"user.creation\",\n\t  \"id\": \"itester\"\n\t}\n}\n"
            )
         )
   }
) RequestDocument<UserCreateRequest> requestDocument
   ) {
      ((UserSpecificSettings)this.userSettings.get()).failIfUserNotEditable();
      Optional<ResourceObject<UserCreateRequest>> resourceObject = Optional.ofNullable(requestDocument.getData());
      Optional<UserCreateRequest> attributes = resourceObject.map(ResourceObject::getAttributes);
      UserAdminService.CreateUser.CreateUserBuilder createCommandBuilder = UserAdminService.CreateUser.builder()
         .userId(resourceObject.map(ResourceObject::getId).map(UserId::userId))
         .password(attributes.map(UserCreateRequest::getPassword).map(PasswordImpl::passwordOf));
      if (attributes.isPresent()) {
         createCommandBuilder.contextData(this.extractContextData(attributes.get()));
      }

      UserAdminService.CreateUser createCommand = createCommandBuilder.build();
      ValidationResult validation = ((UserAdminService)this.userAdminService.get()).validate(createCommand);
      if (!validation.isValid()) {
         return this.errorMapper
            .withErrorCodeMapper(new UserErrorCodeMapper())
            .withStatus(validation.hasError("username", GenericError.NOT_UNIQUE) ? Status.CONFLICT : Status.BAD_REQUEST)
            .withJsonApiAttributesMapper("username")
            .mapToResponse(validation.getErrors());
      }

      UserAdminService.CreationResult result = ((UserAdminService)this.userAdminService.get()).create(createCommand);
      String username = result.username();
      this.helpdeskLogger.log(username, "Administrator ${authentee} created user %s.", new Object[]{LogUtil.quote(username)});
      URI resourceUri = Resources.uri((ExternalUriInfo)this.uriInfo.get(), UserResource.class).build(new Object[]{username});
      Builder<Void> resourceBuilder = ResourceObject.builder("user", username).linkSelf(resourceUri);
      return Response.created(resourceUri).entity(resourceBuilder.document()).build();
   }

   private Map<String, Object> extractContextData(UserCreateRequest attributes) {
      Map<String, Object> contextData = attributes.getContextData();
      if (contextData == null) {
         return Collections.emptyMap();
      }

      ((ContextDataDateConverter)this.dateConverter.get()).transformDates(contextData);
      return contextData;
   }

   private UserStoreQuery createQueryForValidatedParameters(
      PageParameter pageParameter, FilterParameter filterParameter, SortParameter sortParameter, @Nullable Boolean caseSensitive
   ) {
      QuerySettings settings = (QuerySettings)this.querySettings.get();
      if (caseSensitive == null) {
         caseSensitive = settings.caseSensitivityDefault();
      }

      boolean caseInsensitive = !caseSensitive;
      return UserStoreQuery.on((UserStore)this.userStore.get(), (Optional)this.userListDetailLoader.get())
         .withStaticConjunctions(settings.restrictions())
         .withAdditionalFilterRestrictionProviders(settings.restrictionProviders())
         .withContextDataFiltering(collectContextDataFilterColumns(filterParameter))
         .withFilter(filterParameter, caseInsensitive, caseInsensitive && settings.allowsCaseInsensitiveContextDataSearch())
         .withSorter(sortParameter)
         .withPager(pageParameter);
   }

   private void removeParametersWithName(UriBuilder uri, String... names) {
      Stream var10000 = Stream.of(names);
      UriBuilder var3 = uri;
      var10000.forEach(x$0 -> var3.replaceQueryParam(x$0, new Object[0]));
   }

   public static final class HasExportParameter implements Predicate<ExternalUriInfo> {
      public boolean test(ExternalUriInfo uri) {
         return StringUtils.isNotBlank((CharSequence)uri.getQueryParameters().getFirst("export"));
      }
   }
}
