/*
 * Copyright 2025 Apollo Authors
 *
 * 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 com.ctrip.framework.apollo.portal.service;

import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog;
import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluence;
import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTable;
import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLogDataInfluenceTableField;
import com.ctrip.framework.apollo.audit.annotation.OpType;
import com.ctrip.framework.apollo.common.entity.App;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.common.exception.BadRequestException;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.repository.AppNamespaceRepository;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

@Service
public class AppNamespaceService {

  private static final int PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT = 5;
  private static final Joiner APP_NAMESPACE_JOINER = Joiner.on(",").skipNulls();

  private final AppNamespaceRepository appNamespaceRepository;
  private final RoleInitializationService roleInitializationService;
  private final AppService appService;
  private final RolePermissionService rolePermissionService;

  public AppNamespaceService(final AppNamespaceRepository appNamespaceRepository,
      final RoleInitializationService roleInitializationService, final @Lazy AppService appService,
      final RolePermissionService rolePermissionService) {
    this.appNamespaceRepository = appNamespaceRepository;
    this.roleInitializationService = roleInitializationService;
    this.appService = appService;
    this.rolePermissionService = rolePermissionService;
  }

  /**
   * 公共的app ns,能被其它项目关联到的app ns
   */
  public List<AppNamespace> findPublicAppNamespaces() {
    return appNamespaceRepository.findByIsPublicTrue();
  }

  public List<String> findPublicAppNamespaceNames() {
    return appNamespaceRepository.findNamesByIsPublicTrue();
  }

  public AppNamespace findPublicAppNamespace(String namespaceName) {
    List<AppNamespace> appNamespaces =
        appNamespaceRepository.findByNameAndIsPublic(namespaceName, true);

    if (CollectionUtils.isEmpty(appNamespaces)) {
      return null;
    }

    return appNamespaces.get(0);
  }

  private List<AppNamespace> findAllPrivateAppNamespaces(String namespaceName) {
    return appNamespaceRepository.findByNameAndIsPublic(namespaceName, false);
  }

  public AppNamespace findByAppIdAndName(String appId, String namespaceName) {
    return appNamespaceRepository.findByAppIdAndName(appId, namespaceName);
  }

  public List<AppNamespace> findByAppId(String appId) {
    return appNamespaceRepository.findByAppId(appId);
  }

  public List<AppNamespace> findAll() {
    Iterable<AppNamespace> appNamespaces = appNamespaceRepository.findAll();
    return Lists.newArrayList(appNamespaces);
  }

  @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.create",
      description = "createDefaultAppNamespace")
  @Transactional
  public void createDefaultAppNamespace(String appId, String operator) {
    if (!isAppNamespaceNameUnique(appId, ConfigConsts.NAMESPACE_APPLICATION)) {
      throw new BadRequestException("App already has application namespace. AppId = %s", appId);
    }

    AppNamespace appNs = new AppNamespace();
    appNs.setAppId(appId);
    appNs.setName(ConfigConsts.NAMESPACE_APPLICATION);
    appNs.setComment("default app namespace");
    appNs.setFormat(ConfigFileFormat.Properties.getValue());
    appNs.setDataChangeCreatedBy(operator);
    appNs.setDataChangeLastModifiedBy(operator);

    appNamespaceRepository.save(appNs);
  }

  public boolean isAppNamespaceNameUnique(String appId, String namespaceName) {
    Objects.requireNonNull(appId, "AppId must not be null");
    Objects.requireNonNull(namespaceName, "Namespace must not be null");
    return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName));
  }

  @Transactional
  public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, String operator) {
    return createAppNamespaceInLocal(appNamespace, true, operator);
  }

  @Transactional
  @ApolloAuditLog(type = OpType.CREATE, name = "AppNamespace.create",
      description = "createAppNamespaceInLocal")
  public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace,
      boolean appendNamespacePrefix, String operator) {
    String appId = appNamespace.getAppId();

    // add app org id as prefix
    App app = appService.load(appId);
    if (app == null) {
      throw BadRequestException.appNotExists(appId);
    }

    StringBuilder appNamespaceName = new StringBuilder();
    // add prefix postfix
    appNamespaceName
        .append(appNamespace.isPublic() && appendNamespacePrefix ? app.getOrgId() + "." : "")
        .append(appNamespace.getName())
        .append(appNamespace.formatAsEnum() == ConfigFileFormat.Properties ? ""
            : "." + appNamespace.getFormat());
    appNamespace.setName(appNamespaceName.toString());

    if (appNamespace.getComment() == null) {
      appNamespace.setComment("");
    }

    if (!ConfigFileFormat.isValidFormat(appNamespace.getFormat())) {
      throw BadRequestException
          .invalidNamespaceFormat("format must be properties、json、yaml、yml、xml");
    }

    if (StringUtils.isEmpty(appNamespace.getDataChangeCreatedBy())) {
      appNamespace.setDataChangeCreatedBy(operator);
    }

    appNamespace.setDataChangeLastModifiedBy(operator);

    // globally uniqueness check for public app namespace
    if (appNamespace.isPublic()) {
      checkAppNamespaceGlobalUniqueness(appNamespace);
    } else {
      // check private app namespace
      if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(),
          appNamespace.getName()) != null) {
        throw new BadRequestException(
            "Private AppNamespace " + appNamespace.getName() + " already exists!");
      }
      // should not have the same with public app namespace
      checkPublicAppNamespaceGlobalUniqueness(appNamespace);
    }

    AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace);

    roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(),
        operator);
    roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(),
        operator);

    return createdAppNamespace;
  }

  @Transactional
  public AppNamespace importAppNamespaceInLocal(AppNamespace appNamespace) {
    // globally uniqueness check for public app namespace
    if (appNamespace.isPublic()) {
      checkAppNamespaceGlobalUniqueness(appNamespace);
    } else {
      // check private app namespace
      if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(),
          appNamespace.getName()) != null) {
        throw new BadRequestException(
            "Private AppNamespace " + appNamespace.getName() + " already exists!");
      }
      // should not have the same with public app namespace
      checkPublicAppNamespaceGlobalUniqueness(appNamespace);
    }

    AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace);

    String operator = appNamespace.getDataChangeCreatedBy();

    roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(),
        operator);
    roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(),
        operator);

    return createdAppNamespace;
  }

  private void checkAppNamespaceGlobalUniqueness(AppNamespace appNamespace) {
    checkPublicAppNamespaceGlobalUniqueness(appNamespace);

    List<AppNamespace> privateAppNamespaces = findAllPrivateAppNamespaces(appNamespace.getName());

    if (!CollectionUtils.isEmpty(privateAppNamespaces)) {
      Set<String> appIds = Sets.newHashSet();
      for (AppNamespace ans : privateAppNamespaces) {
        appIds.add(ans.getAppId());
        if (appIds.size() == PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT) {
          break;
        }
      }

      throw new BadRequestException("Public AppNamespace " + appNamespace.getName()
          + " already exists as private AppNamespace in appId: " + APP_NAMESPACE_JOINER.join(appIds)
          + ", etc. Please select another name!");
    }
  }

  private void checkPublicAppNamespaceGlobalUniqueness(AppNamespace appNamespace) {
    AppNamespace publicAppNamespace = findPublicAppNamespace(appNamespace.getName());
    if (publicAppNamespace != null) {
      throw new BadRequestException("AppNamespace " + appNamespace.getName()
          + " already exists as public namespace in appId: " + publicAppNamespace.getAppId() + "!");
    }
  }

  @ApolloAuditLog(type = OpType.DELETE, name = "AppNamespace.delete",
      description = "deleteAppNamespace")
  @Transactional
  public AppNamespace deleteAppNamespace(String appId, String namespaceName, String operator) {
    AppNamespace appNamespace = appNamespaceRepository.findByAppIdAndName(appId, namespaceName);
    if (appNamespace == null) {
      throw BadRequestException.appNamespaceNotExists(appId, namespaceName);
    }

    // this operator is passed to
    // com.ctrip.framework.apollo.portal.listener.DeletionListener.onAppNamespaceDeletionEvent
    appNamespace.setDataChangeLastModifiedBy(operator);

    // delete app namespace in portal db
    appNamespaceRepository.delete(appId, namespaceName, operator);

    // delete Permission and Role related data
    rolePermissionService.deleteRolePermissionsByAppIdAndNamespace(appId, namespaceName, operator);

    return appNamespace;
  }

  @ApolloAuditLog(type = OpType.DELETE, name = "AppNamespace.batchDeleteByAppId",
      description = "batchDeleteByAppId")
  public void batchDeleteByAppId(
      @ApolloAuditLogDataInfluence @ApolloAuditLogDataInfluenceTable(tableName = "AppNamespace")
      @ApolloAuditLogDataInfluenceTableField(fieldName = "AppId") String appId, String operator) {
    appNamespaceRepository.batchDeleteByAppId(appId, operator);
  }

}
