package tech.easyflow.skill.gitimport;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.LsRemoteCommand;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.TransportHttp;
import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.transport.http.HttpConnectionFactory;
import org.eclipse.jgit.transport.http.HttpConnectionFactory2;
import org.eclipse.jgit.transport.http.apache.HttpClientConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;

/**
 * 基于 Eclipse JGit 的受控只读仓库读取器。
 */
@Component
public class SkillGitRepositoryReader {

    private static final Logger LOG = LoggerFactory.getLogger(SkillGitRepositoryReader.class);
    private static final String PINNED_REF = "refs/easyflow/skill-import";

    private final GitRepositoryAccessPolicy accessPolicy;
    private final SkillGitImportProperties properties;
    private final SkillGitOperationExecutor operationExecutor;

    /**
     * 创建 Git 仓库读取器。
     *
     * @param accessPolicy 地址访问策略
     * @param properties Git 导入配置
     * @param operationExecutor Git 远程操作执行器
     */
    public SkillGitRepositoryReader(GitRepositoryAccessPolicy accessPolicy,
                                    SkillGitImportProperties properties,
                                    SkillGitOperationExecutor operationExecutor) {
        this.accessPolicy = accessPolicy;
        this.properties = properties;
        this.operationExecutor = operationExecutor;
    }

    /**
     * 读取远程默认分支的当前固定提交。
     *
     * @param repositoryUrl 规范化仓库地址
     * @return 临时只读仓库快照
     * @throws BusinessException 远程仓库不可读取或超过资源限制
     */
    public Snapshot openDefault(String repositoryUrl) {
        return operationExecutor.execute(() -> openDefaultInternal(repositoryUrl), Snapshot::close);
    }

    private Snapshot openDefaultInternal(String repositoryUrl) {
        RemoteHead remoteHead = resolveRemoteHead(repositoryUrl);
        return cloneBranch(repositoryUrl, remoteHead.branchRef(), remoteHead.commitSha());
    }

    /**
     * 重新读取扫描时固定的提交；默认分支已变化时尝试按提交 SHA 获取。
     *
     * @param repositoryUrl 规范化仓库地址
     * @param branchRef 扫描时默认分支完整引用
     * @param commitSha 扫描时完整提交 SHA
     * @return 固定提交快照
     * @throws BusinessException 提交不可读取或仓库超过资源限制
     */
    public Snapshot openCommit(String repositoryUrl, String branchRef, String commitSha) {
        return operationExecutor.execute(
                () -> openCommitInternal(repositoryUrl, branchRef, commitSha), Snapshot::close);
    }

    private Snapshot openCommitInternal(String repositoryUrl, String branchRef, String commitSha) {
        Snapshot branchSnapshot = cloneBranch(repositoryUrl, branchRef, null);
        if (commitSha.equals(branchSnapshot.commitSha())) {
            return branchSnapshot;
        }
        try {
            RefSpec pinned = new RefSpec().setForceUpdate(true)
                    .setSourceDestination(commitSha, PINNED_REF);
            // Git.wrap 与快照共享 Repository；此处不能关闭包装器，否则会提前关闭后续扫描所需的仓库。
            Git.wrap(branchSnapshot.repository()).fetch()
                    .setRemote(Constants.DEFAULT_REMOTE_NAME)
                    .setRefSpecs(pinned)
                    .setDepth(1)
                    .setTagOpt(TagOpt.NO_TAGS)
                    .setTimeout(timeoutSeconds())
                    .setTransportConfigCallback(transportCallback())
                    .call();
            ObjectId pinnedId = branchSnapshot.repository().resolve(PINNED_REF);
            if (pinnedId == null || !commitSha.equals(pinnedId.name())) {
                throw new BusinessException("扫描时的 Git 提交已不可读取，请重新扫描");
            }
            enforceRepositorySize(branchSnapshot.directory());
            return branchSnapshot.withCommit(commitSha);
        } catch (GitAPIException | IOException exception) {
            branchSnapshot.close();
            throw remoteFailure("扫描时的 Git 提交已不可读取，请重新扫描", exception);
        } catch (RuntimeException exception) {
            branchSnapshot.close();
            throw exception;
        }
    }

    private RemoteHead resolveRemoteHead(String repositoryUrl) {
        try {
            LsRemoteCommand command = Git.lsRemoteRepository()
                    .setRemote(repositoryUrl)
                    .setTimeout(timeoutSeconds())
                    .setTransportConfigCallback(transportCallback());
            Map<String, Ref> refs = command.callAsMap();
            Ref head = refs.get(Constants.HEAD);
            if (head == null || head.getObjectId() == null) {
                throw new BusinessException("Git 仓库没有可读取的默认分支");
            }
            Ref branch = resolveHeadBranch(head, refs.values());
            if (branch == null) {
                throw new BusinessException("无法识别 Git 仓库默认分支");
            }
            return new RemoteHead(branch.getName(), head.getObjectId().name());
        } catch (GitAPIException exception) {
            throw remoteFailure("无法读取 Git 仓库，请检查地址和访问权限", exception);
        }
    }

    private Ref resolveHeadBranch(Ref head, Collection<Ref> refs) {
        if (head.isSymbolic() && head.getTarget() != null
                && head.getTarget().getName().startsWith(Constants.R_HEADS)) {
            return head.getTarget();
        }
        return refs.stream()
                .filter(ref -> ref.getName().startsWith(Constants.R_HEADS))
                .filter(ref -> head.getObjectId().equals(ref.getObjectId()))
                .sorted(Comparator.comparingInt(this::branchPriority).thenComparing(Ref::getName))
                .findFirst().orElse(null);
    }

    private int branchPriority(Ref ref) {
        return switch (ref.getName()) {
            case Constants.R_HEADS + "main" -> 0;
            case Constants.R_HEADS + Constants.MASTER -> 1;
            default -> 2;
        };
    }

    private Snapshot cloneBranch(String repositoryUrl, String branchRef, String expectedCommit) {
        Path directory = createTemporaryDirectory();
        Path gitDirectory = directory.resolve("repository.git");
        Repository repository = null;
        try {
            Git git = Git.cloneRepository()
                    .setURI(repositoryUrl)
                    .setDirectory(gitDirectory.toFile())
                    .setBare(true)
                    .setNoCheckout(true)
                    .setBranchesToClone(java.util.List.of(branchRef))
                    .setBranch(branchRef)
                    .setDepth(1)
                    .setTagOption(TagOpt.NO_TAGS)
                    .setTimeout(timeoutSeconds())
                    .setTransportConfigCallback(transportCallback())
                    .call();
            repository = git.getRepository();
            ObjectId head = repository.resolve(Constants.HEAD);
            if (head == null) {
                repository.close();
                deleteDirectory(directory);
                throw new BusinessException("Git 仓库默认分支没有可读取的提交");
            }
            enforceRepositorySize(directory);
            if (expectedCommit != null && !expectedCommit.equals(head.name())) {
                repository.close();
                deleteDirectory(directory);
                throw new BusinessException("Git 仓库默认分支在扫描期间发生变化，请重新扫描");
            }
            return new Snapshot(repository, directory, branchRef, head.name());
        } catch (GitAPIException | IOException exception) {
            if (repository != null) {
                repository.close();
            }
            deleteDirectory(directory);
            throw remoteFailure("无法读取 Git 仓库，请检查地址和访问权限", exception);
        } catch (RuntimeException exception) {
            if (repository != null) {
                repository.close();
            }
            deleteDirectory(directory);
            throw exception;
        }
    }

    private TransportConfigCallback transportCallback() {
        HttpConnectionFactory factory = new PolicyHttpConnectionFactory(
                accessPolicy, properties.getMaxRepositoryBytes(), timeoutMillis());
        int timeout = timeoutSeconds();
        return transport -> configureTransport(transport, factory, timeout);
    }

    private void configureTransport(Transport transport, HttpConnectionFactory factory, int timeout) {
        if (!(transport instanceof TransportHttp httpTransport)) {
            throw new IllegalArgumentException("Git 仓库仅支持 HTTPS 传输");
        }
        transport.setTimeout(timeout);
        httpTransport.setHttpConnectionFactory(factory);
    }

    private int timeoutSeconds() {
        long seconds = Math.max(1L, properties.getTimeout().toSeconds());
        return (int) Math.min(Integer.MAX_VALUE, seconds);
    }

    private int timeoutMillis() {
        long millis = Math.max(1L, properties.getTimeout().toMillis());
        return (int) Math.min(Integer.MAX_VALUE, millis);
    }

    private Path createTemporaryDirectory() {
        try {
            return Files.createTempDirectory("easyflow-skill-git-");
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "创建 Git 仓库临时目录失败", exception);
        }
    }

    private void enforceRepositorySize(Path directory) {
        try (java.util.stream.Stream<Path> paths = Files.walk(directory)) {
            long total = 0L;
            java.util.Iterator<Path> iterator = paths.filter(Files::isRegularFile).iterator();
            while (iterator.hasNext()) {
                total = Math.addExact(total, Files.size(iterator.next()));
                if (total > properties.getMaxRepositoryBytes()) {
                    throw new BusinessException(413, 4132, "Git 仓库超过允许的临时存储大小");
                }
            }
        } catch (ArithmeticException exception) {
            throw new BusinessException(413, 4132, "Git 仓库超过允许的临时存储大小");
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "检查 Git 仓库临时文件失败", exception);
        }
    }

    private BusinessException remoteFailure(String message, Exception exception) {
        return new BusinessException(502, 5021, message, exception);
    }

    private static void deleteDirectory(Path directory) {
        if (directory == null || !Files.exists(directory)) {
            return;
        }
        try (java.util.stream.Stream<Path> paths = Files.walk(directory)) {
            paths.sorted(Comparator.reverseOrder()).forEach(path -> {
                try {
                    Files.deleteIfExists(path);
                } catch (IOException exception) {
                    LOG.warn("清理 Skill Git 临时文件失败，path={}", path, exception);
                }
            });
        } catch (IOException exception) {
            LOG.warn("遍历 Skill Git 临时目录失败，path={}", directory, exception);
        }
    }

    /**
     * 单次固定提交的临时仓库快照。
     */
    public static final class Snapshot implements AutoCloseable {

        private final Repository repository;
        private final Path directory;
        private final String branchRef;
        private final String commitSha;

        Snapshot(Repository repository, Path directory, String branchRef, String commitSha) {
            this.repository = repository;
            this.directory = directory;
            this.branchRef = branchRef;
            this.commitSha = commitSha;
        }

        /**
         * 获取 JGit 仓库。
         *
         * @return 只读仓库
         */
        public Repository repository() {
            return repository;
        }

        /**
         * 获取临时目录。
         *
         * @return 临时目录
         */
        public Path directory() {
            return directory;
        }

        /**
         * 获取默认分支完整引用。
         *
         * @return 分支引用
         */
        public String branchRef() {
            return branchRef;
        }

        /**
         * 获取固定提交 SHA。
         *
         * @return 完整提交 SHA
         */
        public String commitSha() {
            return commitSha;
        }

        private Snapshot withCommit(String pinnedCommit) {
            return new Snapshot(repository, directory, branchRef, pinnedCommit);
        }

        /**
         * 关闭仓库并清理临时目录。
         */
        @Override
        public void close() {
            repository.close();
            deleteDirectory(directory);
        }
    }

    private record RemoteHead(String branchRef, String commitSha) {
    }

    private static final class PolicyHttpConnectionFactory implements HttpConnectionFactory2 {

        private final GitRepositoryAccessPolicy accessPolicy;
        private final long maxResponseBytes;
        private final AtomicLong responseBytes = new AtomicLong();
        private final Map<String, InetAddress[]> pinnedAddresses = new ConcurrentHashMap<>();
        private final CloseableHttpClient httpClient;

        private PolicyHttpConnectionFactory(GitRepositoryAccessPolicy accessPolicy,
                                            long maxResponseBytes,
                                            int timeoutMillis) {
            this.accessPolicy = accessPolicy;
            this.maxResponseBytes = maxResponseBytes;
            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("https", new SSLConnectionSocketFactory(
                            SSLContexts.createSystemDefault(),
                            SSLConnectionSocketFactory.getDefaultHostnameVerifier()))
                    .register("http", PlainConnectionSocketFactory.INSTANCE)
                    .build();
            PoolingHttpClientConnectionManager connectionManager =
                    new PoolingHttpClientConnectionManager(registry, this::resolvePinnedAddress);
            connectionManager.setMaxTotal(4);
            connectionManager.setDefaultMaxPerRoute(2);
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(timeoutMillis)
                    .setConnectionRequestTimeout(timeoutMillis)
                    .setSocketTimeout(timeoutMillis)
                    .setRedirectsEnabled(false)
                    .build();
            this.httpClient = HttpClients.custom()
                    .setConnectionManager(connectionManager)
                    .setDefaultRequestConfig(requestConfig)
                    .disableAutomaticRetries()
                    .disableRedirectHandling()
                    .build();
        }

        @Override
        public HttpConnection create(URL url) throws IOException {
            return create(url, Proxy.NO_PROXY);
        }

        @Override
        public HttpConnection create(URL url, Proxy proxy) throws IOException {
            if (proxy != null && !Proxy.NO_PROXY.equals(proxy)) {
                throw new IOException("Git repository proxy access is disabled");
            }
            GitRepositoryAccessPolicy.ResolvedConnection resolved = accessPolicy.resolveConnection(url);
            pinnedAddresses.putIfAbsent(resolved.host(), resolved.addresses());
            return limited(new HttpClientConnection(url.toString(), Proxy.NO_PROXY, httpClient));
        }

        /**
         * 创建一次 JGit HTTP 会话并在结束时释放专用客户端。
         *
         * @return Git HTTP 会话
         */
        @Override
        public GitSession newSession() {
            return new GitSession() {
                @Override
                public HttpConnection configure(HttpConnection connection, boolean sslVerify) throws IOException {
                    if (!sslVerify) {
                        throw new IOException("Git repository TLS verification must remain enabled");
                    }
                    return connection;
                }

                @Override
                public void close() {
                    try {
                        httpClient.close();
                    } catch (IOException exception) {
                        LOG.warn("关闭 Skill Git HTTP 客户端失败", exception);
                    }
                }
            };
        }

        private InetAddress[] resolvePinnedAddress(String host) throws UnknownHostException {
            InetAddress[] addresses = pinnedAddresses.get(host.toLowerCase(Locale.ROOT));
            if (addresses == null || addresses.length == 0) {
                throw new UnknownHostException("Git repository host was not approved: " + host);
            }
            return addresses.clone();
        }

        private HttpConnection limited(HttpConnection connection) {
            return new LimitedHttpConnection(connection, responseBytes, maxResponseBytes);
        }
    }

    /**
     * 对 JGit HTTP 响应流实施单次远程操作的累计字节上限。
     */
    private static final class LimitedHttpConnection implements HttpConnection {

        private final HttpConnection delegate;
        private final AtomicLong totalBytes;
        private final long maximumBytes;
        private InputStream inputStream;

        private LimitedHttpConnection(HttpConnection delegate, AtomicLong totalBytes, long maximumBytes) {
            this.delegate = delegate;
            this.totalBytes = totalBytes;
            this.maximumBytes = maximumBytes;
        }

        @Override
        public int getResponseCode() throws IOException {
            return delegate.getResponseCode();
        }

        @Override
        public URL getURL() {
            return delegate.getURL();
        }

        @Override
        public String getResponseMessage() throws IOException {
            return delegate.getResponseMessage();
        }

        @Override
        public Map<String, List<String>> getHeaderFields() {
            return delegate.getHeaderFields();
        }

        @Override
        public void setRequestProperty(String key, String value) {
            delegate.setRequestProperty(key, value);
        }

        @Override
        public void setRequestMethod(String method) throws ProtocolException {
            delegate.setRequestMethod(method);
        }

        @Override
        public void setUseCaches(boolean useCaches) {
            delegate.setUseCaches(useCaches);
        }

        @Override
        public void setConnectTimeout(int timeout) {
            delegate.setConnectTimeout(timeout);
        }

        @Override
        public void setReadTimeout(int timeout) {
            delegate.setReadTimeout(timeout);
        }

        @Override
        public String getContentType() {
            return delegate.getContentType();
        }

        @Override
        public InputStream getInputStream() throws IOException {
            if (inputStream == null) {
                int contentLength = delegate.getContentLength();
                if (contentLength > 0 && totalBytes.get() + contentLength > maximumBytes) {
                    throw new IOException("Git repository response exceeds the configured size limit");
                }
                inputStream = new FilterInputStream(delegate.getInputStream()) {
                    @Override
                    public int read() throws IOException {
                        int value = super.read();
                        if (value >= 0) {
                            addBytes(1L);
                        }
                        return value;
                    }

                    @Override
                    public int read(byte[] buffer, int offset, int length) throws IOException {
                        int count = super.read(buffer, offset, length);
                        if (count > 0) {
                            addBytes(count);
                        }
                        return count;
                    }
                };
            }
            return inputStream;
        }

        private void addBytes(long count) throws IOException {
            if (totalBytes.addAndGet(count) > maximumBytes) {
                throw new IOException("Git repository response exceeds the configured size limit");
            }
        }

        @Override
        public String getHeaderField(String name) {
            return delegate.getHeaderField(name);
        }

        @Override
        public List<String> getHeaderFields(String name) {
            return delegate.getHeaderFields(name);
        }

        @Override
        public int getContentLength() {
            return delegate.getContentLength();
        }

        @Override
        public void setInstanceFollowRedirects(boolean followRedirects) {
            delegate.setInstanceFollowRedirects(followRedirects);
        }

        @Override
        public void setDoOutput(boolean doOutput) {
            delegate.setDoOutput(doOutput);
        }

        @Override
        public void setFixedLengthStreamingMode(int contentLength) {
            delegate.setFixedLengthStreamingMode(contentLength);
        }

        @Override
        public OutputStream getOutputStream() throws IOException {
            return delegate.getOutputStream();
        }

        @Override
        public void setChunkedStreamingMode(int chunkLength) {
            delegate.setChunkedStreamingMode(chunkLength);
        }

        @Override
        public String getRequestMethod() {
            return delegate.getRequestMethod();
        }

        @Override
        public boolean usingProxy() {
            return delegate.usingProxy();
        }

        @Override
        public void connect() throws IOException {
            delegate.connect();
        }

        @Override
        public void configure(KeyManager[] keyManagers, TrustManager[] trustManagers,
                              SecureRandom random)
                throws NoSuchAlgorithmException, KeyManagementException {
            delegate.configure(keyManagers, trustManagers, random);
        }

        @Override
        public void setHostnameVerifier(HostnameVerifier verifier)
                throws NoSuchAlgorithmException, KeyManagementException {
            delegate.setHostnameVerifier(verifier);
        }
    }
}
