package io.github.shirainbown.sharelink.reference;

import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Snapshot;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ReactiveExtensionClient;
import io.github.shirainbown.sharelink.model.DownloadResource;
import io.github.shirainbown.sharelink.model.ProtectedLink;

/**
 * Scans published posts for occurrences of {@code /download/{slug}} (download resources)
 * and {@code /link/{slug}} (protected links), producing slug → referencing-posts maps.
 * Results are cached for 5 minutes; a failed post never breaks the whole scan.
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class PostReferenceService {

    private static final String CACHE_KEY_DOWNLOADS = "download-references";
    private static final String CACHE_KEY_LINKS = "link-references";
    private static final Duration CACHE_TTL = Duration.ofMinutes(5);
    private static final int SCAN_CONCURRENCY = 8;

    private final ReactiveExtensionClient client;

    private final Cache<String, Mono<Map<String, List<PostRef>>>> downloadReferenceCache =
        CacheBuilder.newBuilder()
            .expireAfterWrite(CACHE_TTL)
            .maximumSize(1)
            .build();

    private final Cache<String, Mono<Map<String, List<PostRef>>>> linkReferenceCache =
        CacheBuilder.newBuilder()
            .expireAfterWrite(CACHE_TTL)
            .maximumSize(1)
            .build();

    public Mono<Map<String, List<PostRef>>> references() {
        return cached(CACHE_KEY_DOWNLOADS, downloadReferenceCache, () -> scanDownloads());
    }

    public Mono<Map<String, List<PostRef>>> linkReferences() {
        return cached(CACHE_KEY_LINKS, linkReferenceCache, this::scanLinks);
    }

    /**
     * Forces a rescan of download references, discarding the cached result.
     */
    public Mono<Map<String, List<PostRef>>> refresh() {
        downloadReferenceCache.invalidateAll();
        return references();
    }

    /**
     * Forces a rescan of link references, discarding the cached result.
     */
    public Mono<Map<String, List<PostRef>>> refreshLinkReferences() {
        linkReferenceCache.invalidateAll();
        return linkReferences();
    }

    private Mono<Map<String, List<PostRef>>> cached(
        String key, Cache<String, Mono<Map<String, List<PostRef>>>> cache,
        java.util.function.Supplier<Mono<Map<String, List<PostRef>>>> loader) {
        var cached = cache.getIfPresent(key);
        if (cached != null) {
            return cached;
        }
        var loading = loader.get()
            .cache()
            // Never keep a failed scan in the cache.
            .doOnError(e -> cache.invalidate(key));
        cache.put(key, loading);
        return loading;
    }

    private Mono<Map<String, List<PostRef>>> scanDownloads() {
        return client.listAll(DownloadResource.class, new ListOptions(), Sort.unsorted())
            .map(resource -> resource.getSpec() == null ? null : resource.getSpec().getSlug())
            .collectList()
            .flatMap(slugs -> scan("download", Flux.fromIterable(slugs)));
    }

    private Mono<Map<String, List<PostRef>>> scanLinks() {
        return client.listAll(ProtectedLink.class, new ListOptions(), Sort.unsorted())
            .map(link -> link.getSpec() == null ? null : link.getSpec().getSlug())
            .collectList()
            .flatMap(slugs -> scan("link", Flux.fromIterable(slugs)));
    }

    private Mono<Map<String, List<PostRef>>> scan(String marker, Flux<String> slugFlux) {
        return slugFlux
            .filter(StringUtils::isNotBlank)
            .distinct()
            .collectList()
            .flatMap(slugs -> {
                if (slugs.isEmpty()) {
                    return Mono.just(Map.<String, List<PostRef>>of());
                }
                return client.listAll(Post.class, new ListOptions(), Sort.unsorted())
                    .filter(post -> !post.isDeleted()
                        && post.getSpec() != null
                        && StringUtils.isNotBlank(post.getSpec().getReleaseSnapshot()))
                    .flatMap(post -> referencesOfPost(post, slugs, marker)
                            .onErrorResume(e -> {
                                log.warn("扫描文章引用失败，跳过: {}",
                                    post.getMetadata().getName(), e);
                                return Flux.empty();
                            }),
                        SCAN_CONCURRENCY)
                    .collectList()
                    .map(PostReferenceService::groupBySlug);
            });
    }

    private Flux<Map.Entry<String, PostRef>> referencesOfPost(Post post, List<String> slugs,
                                                              String marker) {
        return client.fetch(Snapshot.class, post.getSpec().getReleaseSnapshot())
            .flatMapMany(snapshot -> {
                var snapshotSpec = snapshot.getSpec();
                var content = snapshotSpec == null ? null
                    : StringUtils.firstNonBlank(snapshotSpec.getRawPatch(),
                        snapshotSpec.getContentPatch());
                if (content == null || !content.contains("/" + marker + "/")) {
                    return Flux.empty();
                }
                var ref = PostRef.from(post);
                return Flux.fromIterable(slugs)
                    .filter(slug -> content.contains("/" + marker + "/" + slug))
                    .map(slug -> Map.entry(slug, ref));
            });
    }

    private static Map<String, List<PostRef>> groupBySlug(
        List<Map.Entry<String, PostRef>> entries) {
        Map<String, List<PostRef>> result = new LinkedHashMap<>();
        for (var entry : entries) {
            result.computeIfAbsent(entry.getKey(), key -> new ArrayList<>())
                .add(entry.getValue());
        }
        return result;
    }

    public record PostRef(String postName, String title, String permalink,
                          String editorUrl) {

        static PostRef from(Post post) {
            var name = post.getMetadata().getName();
            var title = post.getSpec() == null || StringUtils.isBlank(post.getSpec().getTitle())
                ? name : post.getSpec().getTitle();
            var permalink = post.getStatus() == null ? null : post.getStatus().getPermalink();
            return new PostRef(name, title, permalink,
                "/console/posts/editor?name=" + name);
        }
    }
}
