package com.tiedup.remake.client.texture;

import com.mojang.blaze3d.platform.NativeImage;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;

/**
 * Manager for dynamically loaded online textures.
 *
 * Features:
 * - Async HTTP download with thread pool
 * - Host whitelist validation
 * - Memory cache with LRU-style expiration
 * - Failed URL tracking to prevent retry spam
 * - Thread-safe operations
 *
 * Singleton pattern - access via getInstance().
 */
@OnlyIn(Dist.CLIENT)
public class DynamicTextureManager {

    private static DynamicTextureManager INSTANCE;

    // Configuration
    private static final int CONNECT_TIMEOUT_MS = 10_000; // 10 seconds
    private static final int READ_TIMEOUT_MS = 30_000; // 30 seconds
    private static final int MAX_CACHE_SIZE = 50;
    private static final long FAILED_URL_RETRY_MS = 5 * 60 * 1000; // 5 minutes

    // Default whitelist of allowed hosts
    private static final Set<String> DEFAULT_WHITELIST = Set.of(
        "i.imgur.com",
        "cdn.discordapp.com",
        "media.discordapp.net",
        "raw.githubusercontent.com",
        "user-images.githubusercontent.com",
        "64.media.tumblr.com",
        "www.minecraftskins.com",
        "minecraftskins.com",
        "textures.minecraft.net",
        "crafatar.com",
        "mc-heads.net",
        "minotar.net",
        "novaskin.me",
        "t.novaskin.me",
        "s.namemc.com",
        "namemc.com"
    );

    // Thread pool for async downloads
    private final Executor downloadExecutor = Executors.newFixedThreadPool(
        2,
        r -> {
            Thread t = new Thread(r, "TiedUp-TextureDownloader");
            t.setDaemon(true);
            return t;
        }
    );

    // Cache: URL -> loaded texture
    private final Map<String, DynamicOnlineTexture> textureCache =
        new ConcurrentHashMap<>();

    // Pending downloads: URL -> future (to avoid duplicate downloads)
    private final Map<
        String,
        CompletableFuture<DynamicOnlineTexture>
    > pendingDownloads = new ConcurrentHashMap<>();

    // Failed URLs: URL -> timestamp of failure (to avoid retry spam)
    private final Map<String, Long> failedUrls = new ConcurrentHashMap<>();

    // Registered textures with Minecraft's texture manager
    private final Map<String, ResourceLocation> registeredTextures =
        new ConcurrentHashMap<>();

    private DynamicTextureManager() {}

    /**
     * Get the singleton instance.
     */
    public static DynamicTextureManager getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new DynamicTextureManager();
        }
        return INSTANCE;
    }

    /**
     * Get a texture for a URL, loading it asynchronously if needed.
     *
     * @param url The texture URL
     * @return The texture if cached, or null if loading/failed
     */
    @Nullable
    public DynamicOnlineTexture getTexture(String url) {
        if (url == null || url.isEmpty()) return null;

        // Check config
        if (!ModConfig.CLIENT.enableDynamicTextures.get()) {
            return null;
        }

        // Check cache
        DynamicOnlineTexture cached = textureCache.get(url);
        if (cached != null) {
            return cached;
        }

        // Check if failed recently
        Long failTime = failedUrls.get(url);
        if (
            failTime != null &&
            System.currentTimeMillis() - failTime < FAILED_URL_RETRY_MS
        ) {
            return null;
        }

        // Start async download if not already pending
        if (!pendingDownloads.containsKey(url)) {
            downloadAsync(url);
        }

        return null;
    }

    /**
     * Check if a texture is loaded and ready.
     */
    public boolean isTextureReady(String url) {
        if (url == null || url.isEmpty()) return false;
        DynamicOnlineTexture tex = textureCache.get(url);
        return tex != null && tex.isValid();
    }

    /**
     * Start an async download for a URL.
     */
    private void downloadAsync(String url) {
        CompletableFuture<DynamicOnlineTexture> future =
            CompletableFuture.supplyAsync(
                () -> {
                    try {
                        return downloadTexture(url);
                    } catch (Exception e) {
                        TiedUpMod.LOGGER.warn(
                            "[DynamicTextureManager] Download failed for {}: {}",
                            url,
                            e.getMessage()
                        );
                        return null;
                    }
                },
                downloadExecutor
            );

        pendingDownloads.put(url, future);

        future.thenAccept(texture -> {
            pendingDownloads.remove(url);

            if (texture != null && texture.isValid()) {
                // Cache management - remove oldest if at capacity
                if (textureCache.size() >= MAX_CACHE_SIZE) {
                    evictOldest();
                }
                textureCache.put(url, texture);
                TiedUpMod.LOGGER.info(
                    "[DynamicTextureManager] Loaded texture from {}",
                    url
                );
            } else {
                failedUrls.put(url, System.currentTimeMillis());
                TiedUpMod.LOGGER.warn(
                    "[DynamicTextureManager] Failed to load texture from {}",
                    url
                );
            }
        });
    }

    /**
     * Download a texture from a URL.
     * Validates host whitelist and performs the HTTP download.
     */
    @Nullable
    private DynamicOnlineTexture downloadTexture(String urlString) {
        try {
            // Validate URL format
            if (!urlString.startsWith("https://")) {
                TiedUpMod.LOGGER.warn(
                    "[DynamicTextureManager] Rejected non-HTTPS URL: {}",
                    urlString
                );
                return null;
            }

            URL url = URI.create(urlString).toURL();
            String host = url.getHost().toLowerCase();

            // Validate host whitelist
            if (ModConfig.CLIENT.useTextureHostWhitelist.get()) {
                if (!isHostWhitelisted(host)) {
                    TiedUpMod.LOGGER.warn(
                        "[DynamicTextureManager] Host not whitelisted: {}",
                        host
                    );
                    return null;
                }
            }

            // Open connection with browser-like headers
            // Many CDNs (including Tumblr) block requests without proper headers
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
            conn.setReadTimeout(READ_TIMEOUT_MS);
            conn.setRequestProperty(
                "User-Agent",
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            );
            conn.setRequestProperty(
                "Accept",
                "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
            );
            conn.setRequestProperty("Accept-Language", "en-US,en;q=0.9");
            conn.setInstanceFollowRedirects(true);

            int responseCode = conn.getResponseCode();
            if (responseCode != 200) {
                TiedUpMod.LOGGER.warn(
                    "[DynamicTextureManager] HTTP {} for {}",
                    responseCode,
                    urlString
                );
                return null;
            }

            String contentType = conn.getContentType();
            TiedUpMod.LOGGER.debug(
                "[DynamicTextureManager] Content-Type: {} for {}",
                contentType,
                urlString
            );

            // Read image - convert any format to PNG for NativeImage compatibility
            try (InputStream is = conn.getInputStream()) {
                // Buffer the stream so we can inspect/retry if needed
                byte[] imageBytes = is.readAllBytes();
                TiedUpMod.LOGGER.info(
                    "[DynamicTextureManager] Downloaded {} bytes (Content-Type: {}) from {}",
                    imageBytes.length,
                    contentType,
                    urlString
                );

                if (imageBytes.length == 0) {
                    TiedUpMod.LOGGER.warn(
                        "[DynamicTextureManager] Empty response from {}",
                        urlString
                    );
                    return null;
                }

                // Check if response looks like HTML (error page) instead of image
                if (imageBytes.length > 0 && imageBytes[0] == '<') {
                    String preview = new String(
                        imageBytes,
                        0,
                        Math.min(200, imageBytes.length)
                    );
                    TiedUpMod.LOGGER.warn(
                        "[DynamicTextureManager] Response looks like HTML, not an image: {}",
                        preview
                    );
                    return null;
                }

                NativeImage image = readImageAsNative(
                    new ByteArrayInputStream(imageBytes),
                    urlString
                );
                if (image == null) {
                    // Log first bytes to help debug
                    StringBuilder hexPreview = new StringBuilder();
                    for (int i = 0; i < Math.min(16, imageBytes.length); i++) {
                        hexPreview.append(
                            String.format("%02X ", imageBytes[i])
                        );
                    }
                    TiedUpMod.LOGGER.warn(
                        "[DynamicTextureManager] First 16 bytes: {}",
                        hexPreview
                    );
                    return null;
                }
                return new DynamicOnlineTexture(image, urlString);
            }
        } catch (Exception e) {
            TiedUpMod.LOGGER.error(
                "[DynamicTextureManager] Error downloading {}: {}",
                urlString,
                e.getMessage()
            );
            return null;
        }
    }

    /**
     * Read an image from InputStream, converting any format (JPG, PNG, etc.) to NativeImage.
     * NativeImage only supports PNG natively, so we use ImageIO to read other formats
     * and convert them to PNG bytes.
     *
     * @param is The input stream containing image data
     * @param urlString The URL (for logging)
     * @return NativeImage or null if failed
     */
    @Nullable
    private NativeImage readImageAsNative(InputStream is, String urlString) {
        try {
            // First, try reading with ImageIO (supports JPG, PNG, GIF, BMP, etc.)
            BufferedImage bufferedImage = ImageIO.read(is);
            if (bufferedImage == null) {
                // Try to peek at the first bytes to see what we got
                TiedUpMod.LOGGER.warn(
                    "[DynamicTextureManager] ImageIO could not read image from {} - format may be unsupported or data is not an image",
                    urlString
                );
                return null;
            }

            TiedUpMod.LOGGER.debug(
                "[DynamicTextureManager] Read image {}x{} from {}",
                bufferedImage.getWidth(),
                bufferedImage.getHeight(),
                urlString
            );

            // Convert BufferedImage to PNG bytes, then to NativeImage
            ByteArrayOutputStream pngOutput = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "PNG", pngOutput);
            byte[] pngBytes = pngOutput.toByteArray();

            TiedUpMod.LOGGER.debug(
                "[DynamicTextureManager] Converted to {} PNG bytes",
                pngBytes.length
            );

            // Now read as NativeImage (PNG format is supported)
            try (
                ByteArrayInputStream pngInput = new ByteArrayInputStream(
                    pngBytes
                )
            ) {
                return NativeImage.read(pngInput);
            }
        } catch (Exception e) {
            TiedUpMod.LOGGER.error(
                "[DynamicTextureManager] Failed to convert image from {}: {}",
                urlString,
                e.getMessage(),
                e
            );
            return null;
        }
    }

    /**
     * Check if a host is in the whitelist.
     */
    private boolean isHostWhitelisted(String host) {
        // Check default whitelist
        if (DEFAULT_WHITELIST.contains(host)) {
            return true;
        }

        // Check config additional whitelist
        for (String allowed : ModConfig.CLIENT.textureHostWhitelist.get()) {
            if (
                host.equals(allowed.toLowerCase()) ||
                host.endsWith("." + allowed.toLowerCase())
            ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Evict the oldest texture from cache.
     */
    private void evictOldest() {
        // Simple eviction: remove first entry
        var iterator = textureCache.entrySet().iterator();
        if (iterator.hasNext()) {
            var entry = iterator.next();
            entry.getValue().close();
            iterator.remove();
            registeredTextures.remove(entry.getKey());
        }
    }

    /**
     * Get or create a ResourceLocation for a dynamic texture.
     * Registers the texture with Minecraft's texture manager.
     *
     * @param url The texture URL
     * @param fullSkin Whether to use full-skin mode texture
     * @return The ResourceLocation, or null if not available
     */
    @Nullable
    public ResourceLocation getTextureLocation(String url, boolean fullSkin) {
        DynamicOnlineTexture texture = getTexture(url);
        if (texture == null || !texture.isValid()) return null;

        String key = url + (fullSkin ? "_fullskin" : "_clothes");

        return registeredTextures.computeIfAbsent(key, k -> {
            var dynTex = fullSkin
                ? texture.getFullSkinTexture()
                : texture.getClothesTexture();
            if (dynTex == null) return null;

            // Create unique ResourceLocation
            int hash = Math.abs(key.hashCode());
            ResourceLocation loc = ResourceLocation.fromNamespaceAndPath(
                TiedUpMod.MOD_ID,
                "dynamic/clothes_" + hash
            );

            // Register with Minecraft
            Minecraft.getInstance().getTextureManager().register(loc, dynTex);
            return loc;
        });
    }

    /**
     * Invalidate a specific URL from cache.
     */
    public void invalidate(String url) {
        DynamicOnlineTexture tex = textureCache.remove(url);
        if (tex != null) {
            tex.close();
        }
        pendingDownloads.remove(url);
        failedUrls.remove(url);
        registeredTextures.remove(url + "_clothes");
        registeredTextures.remove(url + "_fullskin");
    }

    /**
     * Clear all caches. Called on world unload.
     */
    public void clearAll() {
        for (DynamicOnlineTexture tex : textureCache.values()) {
            tex.close();
        }
        textureCache.clear();
        pendingDownloads.clear();
        failedUrls.clear();
        registeredTextures.clear();
        TiedUpMod.LOGGER.info(
            "[DynamicTextureManager] Cleared all texture caches"
        );
    }

    /**
     * Clean up stale failed URL entries.
     */
    public void cleanupStale() {
        long now = System.currentTimeMillis();
        failedUrls
            .entrySet()
            .removeIf(e -> now - e.getValue() > FAILED_URL_RETRY_MS * 2);
    }

    /**
     * Get cache statistics for debugging.
     */
    public String getCacheStats() {
        return String.format(
            "Cache: %d textures, %d pending, %d failed",
            textureCache.size(),
            pendingDownloads.size(),
            failedUrls.size()
        );
    }
}
