/*
 * Copyright 2022 the original author or 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
 *
 *      https://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.initializer.protocol.archive;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.initializer.core.ProjectGenerationRequest;
import com.alibaba.initializer.core.ProjectGenerationResult;
import com.alibaba.initializer.core.ProjectGenerator;
import com.alibaba.initializer.core.constants.ErrorCodeEnum;
import com.alibaba.initializer.core.exception.BizRuntimeException;
import com.alibaba.initializer.protocol.resolver.RequestConverter;
import com.google.common.collect.Lists;
import io.spring.initializr.web.controller.ProjectGenerationController;
import io.spring.initializr.web.project.ProjectRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.UnixStat;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor;

/**
 * backage generated code and output with http protocol
 *
 * @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
 */
@Slf4j
public class ProjectArchiveHandler {

    @Autowired
    private ProjectGenerator routeGenerator;

    @Autowired
    private RequestConverter requestConverter;

    private HttpEntityMethodProcessor httpEntityMethodProcessor = new HttpEntityMethodProcessor(Lists.newArrayList(new ByteArrayHttpMessageConverter()));

    private void responseArchiveBytes(ResponseEntity<byte[]> entity, HttpServletRequest request, HttpServletResponse response) {

        try {
            // using spring compone to response archive bytes
            Method method = ProjectGenerationController.class.getDeclaredMethod("springZip", ProjectRequest.class);

            MethodParameter parameter = new MethodParameter(method, -1);

            httpEntityMethodProcessor.handleReturnValue(entity, parameter, new ModelAndViewContainer(), new ServletWebRequest(request, response));
        } catch (Exception e) {
            log.error("write archive error", e);
        }
    }

    public void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String protocol) {
        ProjectGenerationRequest projRequest = requestConverter.convert(request);

        ProjectGenerationResult projResult = routeGenerator.generate(projRequest);

        // create archive bytes
        ResponseEntity<byte[]> entity;
        Path archive = null;
        try {
            switch (protocol) {
                case "zip":
                    archive = createArchive(projResult, "zip", ZipArchiveOutputStream::new, ZipArchiveEntry::new, ZipArchiveEntry::setUnixMode);
                    entity = upload(archive, generateFileName(projResult, "zip"), "application/zip");
                    break;
                case "tar":
                case "tar.gz":
                    archive = createArchive(projResult, "tar.gz", this::createTarArchiveOutputStream, TarArchiveEntry::new, TarArchiveEntry::setMode);
                    entity = upload(archive, generateFileName(projResult, "tar.gz"), "application/x-compress");
                    break;
                default:
                    throw new BizRuntimeException(ErrorCodeEnum.UNSUPPORTED, "not support archive type");
            }
        } catch (Exception e) {
            throw new BizRuntimeException(ErrorCodeEnum.SYSTEM_ERROR, "create archive error", e);
        } finally {
            projResult.cleanUp();
            try {
                FileSystemUtils.deleteRecursively(archive);
            } catch (IOException ignoe) {
            }
        }

        // response archive bytes
        responseArchiveBytes(entity, request, response);
    }


    private String generateFileName(ProjectGenerationResult request, String extension) {
        String candidate = request.getName();
        String tmp = candidate.replaceAll(" ", "_");
        try {
            return URLEncoder.encode(tmp, "UTF-8") + "." + extension;
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalStateException("Cannot encode URL", ex);
        }
    }

    private ResponseEntity<byte[]> upload(Path archive, String fileName, String contentType)
            throws IOException {
        byte[] bytes = Files.readAllBytes(archive);
        ResponseEntity<byte[]> result = createResponseEntity(bytes, contentType, fileName);
        return result;
    }

    private ResponseEntity<byte[]> createResponseEntity(byte[] content, String contentType, String fileName) {
        String contentDispositionValue = "attachment; filename=\"" + fileName + "\"";
        return ResponseEntity.ok().header("Content-Type", contentType)
                .header("Content-Disposition", contentDispositionValue).body(content);
    }

    private TarArchiveOutputStream createTarArchiveOutputStream(OutputStream output) {
        try {
            return new TarArchiveOutputStream(new GzipCompressorOutputStream(output));
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private <T extends ArchiveEntry> Path createArchive(ProjectGenerationResult result, String fileExtension,
                                                        Function<OutputStream, ? extends ArchiveOutputStream> archiveOutputStream,
                                                        BiFunction<File, String, T> archiveEntry, BiConsumer<T, Integer> setMode) throws IOException {
        Path root = result.getProjectRoot();
        Path archive = root.resolveSibling(root.getFileName() + "." + fileExtension);
        try (ArchiveOutputStream output = archiveOutputStream.apply(Files.newOutputStream(archive))) {
            Files.walk(result.getProjectRoot())
                    .filter((path) -> !result.getProjectRoot().equals(path))
                    .forEach((path) -> {
                        try {
                            String entryName = getEntryName(result.getProjectRoot(), path);
                            T entry = archiveEntry.apply(path.toFile(), entryName);
                            setMode.accept(entry, getUnixMode(path));
                            output.putArchiveEntry(entry);
                            if (!Files.isDirectory(path)) {
                                Files.copy(path, output);
                            }
                            output.closeArchiveEntry();
                        } catch (IOException ex) {
                            throw new IllegalStateException(ex);
                        }
                    });
        }
        return archive;
    }

    private String getEntryName(Path root, Path path) {
        String entryName = root.relativize(path).toString().replace('\\', '/');
        if (Files.isDirectory(path)) {
            entryName += "/";
        }
        return entryName;
    }

    private int getUnixMode(Path path) {
        if (Files.isDirectory(path)) {
            return UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
        } else {
            return UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM;
        }
    }

}
