ImageSearchService.kt

package nl.johnvanweel.kikker

import org.slf4j.LoggerFactory
import tools.jackson.databind.ObjectMapper
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import java.io.File
import java.security.MessageDigest
import java.util.Base64

@Service
class ImageSearchService(private val restClient: RestClient) {
    private val logger = LoggerFactory.getLogger(javaClass)

    private val fallbackImageBase64 by lazy {
        val bytes = this::class.java.getResourceAsStream("/static/boot.jpg")?.readAllBytes()
            ?: throw RuntimeException("Fallback image not found")
        Base64.getEncoder().encodeToString(bytes)
    }

    fun searchRandomImageBase64(query: String): String {
        return try {
            performSearch(query)
        } catch (e: Exception) {
            val cachedImage = getFallbackFromCache()
            if (cachedImage != null) {
                logger.info("Image search failed: {}. Using random image from cache.", e.message)
                Base64.getEncoder().encodeToString(cachedImage)
            } else {
                println("[ERROR] Image search failed: ${e.message}. No cached images available. Falling back to default image.")
                fallbackImageBase64
            }
        }
    }

    private fun getFallbackFromCache(): ByteArray? {
        val cacheDir = File("/tmp/kikker")
        if (!cacheDir.exists() || !cacheDir.isDirectory) {
            return null
        }

        val files = cacheDir.listFiles()?.filter { it.isFile }
        if (files.isNullOrEmpty()) {
            return null
        }

        return files.random().readBytes()
    }

    private fun performSearch(query: String): String {
        // Step 1: Get VQD token
        val initialResponse = restClient.get()
            .uri("https://duckduckgo.com/?q={query}", query)
            .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .retrieve()
            .body(String::class.java) ?: ""

        val vqdRegex = "vqd=[\"']?([^\"'&]+)".toRegex()
        val vqd = vqdRegex.find(initialResponse)
            ?.groupValues?.get(1)
            ?: throw RuntimeException("Could not find vqd token")

        // Step 2: Search for images
        val searchResponse = restClient.get()
            .uri("https://duckduckgo.com/i.js?q={query}&o=json&vqd={vqd}", query, vqd)
            .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .header("Referer", "https://duckduckgo.com/")
            .header("Accept", "*/*")
            .retrieve()
            .body(String::class.java) ?: throw RuntimeException("Empty search response")

        val objectMapper = ObjectMapper()
        val bodyMap = objectMapper.readValue(searchResponse, Map::class.java)

        val results = bodyMap?.get("results") as? List<Map<String, Any>>
        val randomImageUrl = results?.randomOrNull()?.get("image") as? String
            ?: throw RuntimeException("No image found")

        // Step 3: Fetch the image bytes (with cache)
        val imageBytes = getImageBytes(randomImageUrl)

        return Base64.getEncoder().encodeToString(imageBytes)
    }

    private fun getImageBytes(imageUrl: String): ByteArray {
        val hash = sha256(imageUrl)
        val cacheDir = File("/tmp/kikker")
        if (!cacheDir.exists()) {
            cacheDir.mkdirs()
        }
        val cacheFile = File(cacheDir, hash)

        if (cacheFile.exists()) {
            logger.info("Cache hit for image: {}", imageUrl)
            return cacheFile.readBytes()
        }

        logger.info("Cache miss for image: {}. Downloading...", imageUrl)
        val imageBytes = restClient.get()
            .uri(imageUrl)
            .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
            .retrieve()
            .body(ByteArray::class.java) ?: throw RuntimeException("Could not fetch image")

        cacheFile.writeBytes(imageBytes)
        logger.info("Image cached: {} -> {}", imageUrl, cacheFile.absolutePath)
        return imageBytes
    }

    private fun sha256(input: String): String {
        val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
        return bytes.joinToString("") { "%02x".format(it) }
    }
}