package ca.ksamad.encore.playback

import coil3.ImageLoader
import coil3.PlatformContext
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.key.Keyer
import coil3.request.Options
import okio.Buffer
import okio.FileSystem
import okio.Path.Companion.toOkioPath

/**
 * Builds the Coil [ImageLoader] that serves MPD cover art. Art bytes come from
 * [MpdConnectionManager.fetchArt] via a custom [Fetcher].
 *
 * Coil's disk cache is only auto-managed by its network fetchers, so a custom fetcher must
 * read/write the [DiskCache] itself — otherwise art is only memory-cached and every cold start
 * re-fetches from the server. [MpdArtFetcher] therefore checks the disk cache first, and
 * write-through-caches misses, so each cover is fetched from the (tiny) server exactly once, ever.
 */
object ArtImageLoader {
    fun create(
        context: PlatformContext,
        manager: MpdConnectionManager,
    ): ImageLoader =
        ImageLoader.Builder(context)
            .components {
                add(MpdArtKeyer(), MpdArtData::class)
                add(MpdArtFetcher.Factory { manager.fetchArt(it) }, MpdArtData::class)
            }
            .diskCache {
                DiskCache.Builder()
                    .directory(context.cacheDir.resolve("mpd_art").toOkioPath())
                    .maxSizeBytes(64L * 1024 * 1024)
                    .build()
            }
            .build()

    /**
     * Stable cache key per art request; shared by the keyer and the disk cache. For [SongArt] the
     * optional [SongArt.version] is appended so a re-tagged file (new mtime) lands on a fresh key
     * and refetches; album keys stay version-free so an album's tracks share one entry.
     */
    private fun cacheKey(data: MpdArtData): String =
        when (data) {
            is SongArt ->
                if (data.version.isNullOrEmpty()) "song:${data.uri}"
                else "song:${data.uri}@${data.version}"
            is AlbumArt -> "album:${data.albumArtist.orEmpty()}/${data.album}"
        }

    private class MpdArtKeyer : Keyer<MpdArtData> {
        override fun key(
            data: MpdArtData,
            options: Options,
        ): String = cacheKey(data)
    }

    private class MpdArtFetcher(
        private val data: MpdArtData,
        private val diskCache: DiskCache?,
        private val fetch: suspend (MpdArtData) -> ByteArray?,
    ) : Fetcher {
        override suspend fun fetch(): FetchResult? {
            val key = cacheKey(data)

            // 1. Serve from disk cache if present (survives process death).
            diskCache?.openSnapshot(key)?.let { snapshot ->
                return sourceResult(snapshot, key, DataSource.DISK)
            }

            // 2. Miss → fetch from the server.
            val bytes = fetch(data) ?: return null

            // 3. Write-through to the disk cache, then serve from it.
            writeToDiskCache(key, bytes)?.let { snapshot ->
                return sourceResult(snapshot, key, DataSource.NETWORK)
            }

            // 4. Fallback (no disk cache / write failed): serve from memory.
            return SourceFetchResult(
                source = ImageSource(Buffer().apply { write(bytes) }, FileSystem.SYSTEM),
                mimeType = null,
                dataSource = DataSource.NETWORK,
            )
        }

        private fun writeToDiskCache(
            key: String,
            bytes: ByteArray,
        ): DiskCache.Snapshot? {
            val cache = diskCache ?: return null
            val editor = cache.openEditor(key) ?: return null
            return try {
                cache.fileSystem.write(editor.data) { write(bytes) }
                editor.commitAndOpenSnapshot()
            } catch (e: Exception) {
                editor.abort()
                null
            }
        }

        private fun sourceResult(
            snapshot: DiskCache.Snapshot,
            key: String,
            dataSource: DataSource,
        ): SourceFetchResult =
            SourceFetchResult(
                source =
                    ImageSource(
                        file = snapshot.data,
                        fileSystem = diskCache!!.fileSystem,
                        diskCacheKey = key,
                        closeable = snapshot,
                    ),
                mimeType = null,
                dataSource = dataSource,
            )

        class Factory(private val fetch: suspend (MpdArtData) -> ByteArray?) :
            Fetcher.Factory<MpdArtData> {
            override fun create(
                data: MpdArtData,
                options: Options,
                imageLoader: ImageLoader,
            ): Fetcher = MpdArtFetcher(data, imageLoader.diskCache, fetch)
        }
    }
}
