/*
 * Copyright 1999-2025 Alibaba Group Holding Ltd.
 *
 * 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.alibaba.nacos.console.handler.impl.remote.config;

import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.model.SameConfigPolicy;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException;
import com.alibaba.nacos.api.model.v2.Result;
import com.alibaba.nacos.auth.config.NacosAuthConfig;
import com.alibaba.nacos.auth.config.NacosAuthConfigHolder;
import com.alibaba.nacos.common.http.HttpUtils;
import com.alibaba.nacos.common.http.param.Query;
import com.alibaba.nacos.common.utils.IoUtils;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.config.server.utils.RequestUtil;
import com.alibaba.nacos.console.config.NacosConsoleAuthConfig;
import com.alibaba.nacos.console.handler.impl.remote.EnabledRemoteHandler;
import com.alibaba.nacos.core.cluster.Member;
import com.alibaba.nacos.core.cluster.NacosMemberManager;
import com.alibaba.nacos.core.utils.WebUtils;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.hc.client5.http.HttpResponseException;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler;
import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.ProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Nacos config import and export service.
 *
 * @author xiweng.yy
 */
@Service
@EnabledRemoteHandler
public class ConfigImportAndExportService {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigImportAndExportService.class);
    
    private static final String REMOTE_CONFIG_IMPORT_URL = "http://%s%s/v3/admin/cs/config/import";
    
    private static final String REMOTE_CONFIG_EXPORT_URL = "http://%s%s/v3/admin/cs/config/export";
    
    private final NacosMemberManager memberManager;
    
    public ConfigImportAndExportService(NacosMemberManager memberManager) {
        this.memberManager = memberManager;
    }
    
    /**
     * Do import config to remote server.
     *
     * @param sourceUser    source user from console request
     * @param namespaceId   namespace id from console request
     * @param policy        conflict policy
     * @param importFile    imported config file
     * @param sourceIp      source ip from console request
     * @param sourceApp     source app from console request
     * @return Maps of import success and failed count
     */
    public Result<Map<String, Object>> importConfig(String sourceUser, String namespaceId, SameConfigPolicy policy,
            MultipartFile importFile, String sourceIp, String sourceApp) {
        String serverContextPath = getServerContextPath();
        Member serverMember = randomOneMember();
        String url = String.format(REMOTE_CONFIG_IMPORT_URL, serverMember.getAddress(), serverContextPath);
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            Query query = Query.newInstance().addParam("namespaceId", namespaceId).addParam("srcUser", sourceUser);
            URI uri = HttpUtils.buildUri(url, query);
            HttpPost httpPost = new HttpPost(uri);
            httpPost.setHeader(WebUtils.X_FORWARDED_FOR, sourceIp);
            httpPost.setHeader(RequestUtil.CLIENT_APPNAME_HEADER, sourceApp);
            addAuthIdentity(httpPost);
            String contentTypeString = null == importFile.getContentType() ? MediaType.MULTIPART_FORM_DATA_VALUE
                    : importFile.getContentType();
            ContentType contentType = ContentType.create(contentTypeString, Constants.ENCODE);
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            multipartEntityBuilder.addBinaryBody("file", importFile.getInputStream(), contentType,
                    importFile.getOriginalFilename());
            multipartEntityBuilder.addTextBody("policy", policy.name(), contentType);
            HttpEntity entity = multipartEntityBuilder.build();
            httpPost.setEntity(entity);
            String executeResult = httpClient.execute(httpPost, new BasicHttpClientResponseHandler());
            return JacksonUtils.toObj(executeResult, new TypeReference<>() {
            });
        } catch (HttpResponseException responseException) {
            LOGGER.error("Import config to server {} failed with code {}: ", serverMember.getAddress(),
                    responseException.getStatusCode());
            throw new NacosRuntimeException(responseException.getStatusCode(), responseException.getMessage());
        } catch (IOException | URISyntaxException e) {
            LOGGER.error("Import config to server {} failed: ", serverMember.getAddress(), e);
            throw new NacosRuntimeException(NacosException.SERVER_ERROR, "Import config to server failed.");
        }
    }
    
    /**
     * Do export config to from server.
     *
     * @param dataId        data id of export config
     * @param group         group name of export config
     * @param namespaceId   namespace of export config
     * @param appName       app name of export config
     * @param ids           storage id of export config
     * @return export file entity
     * @throws Exception    any exception during export config
     */
    public ResponseEntity<byte[]> exportConfig(String dataId, String group, String namespaceId, String appName,
            List<Long> ids) throws Exception {
        String serverContextPath = getServerContextPath();
        Member serverMember = randomOneMember();
        String url = String.format(REMOTE_CONFIG_EXPORT_URL, serverMember.getAddress(), serverContextPath);
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            Query query = Query.newInstance().addParam("namespaceId", namespaceId).addParam("dataId", dataId)
                    .addParam("groupName", group).addParam("ids", StringUtils.join(ids, ","));
            URI uri = HttpUtils.buildUri(url, query);
            HttpGet httpGet = new HttpGet(uri);
            addAuthIdentity(httpGet);
            return httpClient.execute(httpGet, new ExportHttpClientResponseHandler());
        } catch (HttpResponseException responseException) {
            LOGGER.error("Export config from server {} failed with code {}: ", serverMember.getAddress(),
                    responseException.getStatusCode());
            throw new NacosRuntimeException(responseException.getStatusCode(), responseException.getMessage());
        } catch (IOException | URISyntaxException e) {
            LOGGER.error("Export config from server {} failed: ", serverMember.getAddress(), e);
            throw new NacosRuntimeException(NacosException.SERVER_ERROR, "Export config to server failed.");
        }
    }
    
    private void addAuthIdentity(HttpRequest request) {
        NacosAuthConfig authConfig = NacosAuthConfigHolder.getInstance()
                .getNacosAuthConfigByScope(NacosConsoleAuthConfig.NACOS_CONSOLE_AUTH_SCOPE);
        if (StringUtils.isNotBlank(authConfig.getServerIdentityKey())) {
            request.setHeader(authConfig.getServerIdentityKey(), authConfig.getServerIdentityValue());
        }
    }
    
    private String getServerContextPath() {
        return EnvUtil.getProperty("nacos.console.remote.server.context-path", "/nacos");
    }
    
    private Member randomOneMember() {
        Collection<Member> allMembers = memberManager.allMembers();
        return allMembers.parallelStream().findAny().orElseThrow();
    }
    
    static class ExportHttpClientResponseHandler
            extends AbstractHttpClientResponseHandler<ResponseEntity<byte[]>> {
        
        private String contentDisposition;
        
        @Override
        public ResponseEntity<byte[]> handleResponse(ClassicHttpResponse response) throws IOException {
            try {
                contentDisposition = response.getHeader("Content-Disposition").getValue();
            } catch (ProtocolException e) {
                throw new NacosRuntimeException(NacosException.SERVER_ERROR,
                        "Export config from server, parse response file name failed; ", e);
            }
            return super.handleResponse(response);
        }
        
        @Override
        public ResponseEntity<byte[]> handleEntity(HttpEntity entity) throws IOException {
            InputStream inputStream = entity.getContent();
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                IoUtils.copy(inputStream, outputStream);
                byte[] responseBody = outputStream.toByteArray();
                return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM)
                        .header("Content-Disposition", contentDisposition).body(responseBody);
            }
        }
    }
}
