package com.tencent.qian.webview.prewarm;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * manifest 拉取 / 缓存 / 新鲜度管理（进程内，由 Prewarmer 持有）。
 *
 * <p>策略（design.md Decision 5）：内存缓存整个 session；TTL 读自响应头 {@code Cache-Control: max-age}，
 * 缺省 300s；{@link #ensureFresh} 缺失则拉取并排队回调（pending replay），已有但过期则后台重拉
 * （stale-while-revalidate）；{@code isFetching} 保护每 TTL 窗口最多一次重拉；拉取失败静默。
 *
 * <p>回调在后台线程触发；涉及 WebView 的动作由调用方切主线程。
 */
public final class ManifestStore {

    /** manifest 就绪回调。 */
    public interface OnReady {
        void onReady(@NonNull WarmupManifest manifest);
    }

    private volatile WarmupManifest manifest = null;
    private volatile long lastFetchAtMs = 0L;
    private volatile long ttlMs = 300_000L;
    private volatile String manifestUrl = "";
    private final AtomicBoolean isFetching = new AtomicBoolean(false);

    private final List<OnReady> pending = new ArrayList<>();
    private final Object pendingLock = new Object();

    /** pending 回调队列上限。弱网下 ensureFresh 可能多次入队，设上限防御异常累积。 */
    private static final int MAX_PENDING = 16;

    @Nullable
    private volatile Runnable onVersionChanged = null;

    private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "qian-prewarm-manifest");
        t.setDaemon(true);
        return t;
    });

    public void setManifestUrl(@NonNull String url) {
        this.manifestUrl = url;
    }

    public void setOnVersionChanged(@Nullable Runnable r) {
        this.onVersionChanged = r;
    }

    @Nullable
    public WarmupManifest getManifest() {
        return manifest;
    }

    public boolean isStale() {
        return (System.currentTimeMillis() - lastFetchAtMs) > ttlMs;
    }

    /** 确保 manifest 可用且新鲜。 */
    public void ensureFresh(@Nullable OnReady onReady) {
        WarmupManifest current = manifest;
        if (current != null) {
            if (onReady != null) {
                onReady.onReady(current);
            }
            if (isStale()) {
                fetch();
            }
            return;
        }
        if (onReady != null) {
            // 上限保护：超限直接丢弃新回调，避免弱网下 pending 无界增长。
            synchronized (pendingLock) {
                if (pending.size() < MAX_PENDING) {
                    pending.add(onReady);
                }
            }
        }
        fetch();
    }

    private void fetch() {
        final String url = manifestUrl;
        if (url.isEmpty()) {
            return;
        }
        if (!isFetching.compareAndSet(false, true)) {
            return;
        }
        executor.execute(() -> {
            HttpURLConnection conn = null;
            boolean success = false;
            try {
                conn = (HttpURLConnection) new URL(url).openConnection();
                conn.setRequestMethod("GET");
                conn.setConnectTimeout(8000);
                conn.setReadTimeout(8000);
                conn.setRequestProperty("Accept", "application/json");
                int code = conn.getResponseCode();
                if (code != 200) {
                    return;
                }
                Long parsedTtl = parseMaxAge(conn.getHeaderField("Cache-Control"));
                ttlMs = parsedTtl != null ? parsedTtl : 300_000L;
                StringBuilder sb = new StringBuilder();
                try (BufferedReader br = new BufferedReader(
                        new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
                    String line;
                    while ((line = br.readLine()) != null) {
                        sb.append(line);
                    }
                }
                WarmupManifest parsed = WarmupManifest.parse(sb.toString());
                if (parsed == null) {
                    return;
                }
                String oldVersion = manifest != null ? manifest.version : "";
                boolean versionChanged = !oldVersion.isEmpty() && !oldVersion.equals(parsed.version);
                manifest = parsed;
                lastFetchAtMs = System.currentTimeMillis();
                success = true;
                List<OnReady> callbacks;
                synchronized (pendingLock) {
                    callbacks = new ArrayList<>(pending);
                    pending.clear();
                }
                for (OnReady cb : callbacks) {
                    try {
                        cb.onReady(parsed);
                    } catch (Throwable ignored) {
                    }
                }
                if (versionChanged && onVersionChanged != null) {
                    onVersionChanged.run();
                }
            } catch (Throwable ignored) {
                // 拉取/解析失败静默
            } finally {
                if (conn != null) {
                    try {
                        conn.disconnect();
                    } catch (Throwable ignored) {
                    }
                }
                isFetching.set(false);
                // 拉取失败：清空 pending（符合"失败静默不回调"契约），避免回调无界累积。
                // 下次 ensureFresh 会重新入队重试；去重状态未更新故不会误判已预热。
                if (!success) {
                    synchronized (pendingLock) {
                        pending.clear();
                    }
                }
            }
        });
    }

    /** 解析 {@code Cache-Control: max-age=NNN} → 毫秒；无则返回 null。 */
    @Nullable
    static Long parseMaxAge(@Nullable String headerValue) {
        if (headerValue == null || headerValue.isEmpty()) {
            return null;
        }
        String cc = headerValue.toLowerCase();
        int idx = cc.indexOf("max-age=");
        if (idx < 0) {
            return null;
        }
        String rest = cc.substring(idx + 8);
        String token = rest.split(",")[0].trim();
        try {
            long seconds = Long.parseLong(token);
            if (seconds <= 0) {
                return null;
            }
            return seconds * 1000L;
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
