package cy.hexor.furumi.data.local

import android.content.Context
import cy.hexor.furumi.domain.model.TrackCard
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request

@Singleton
class OfflineMediaStore @Inject constructor(
    @ApplicationContext context: Context,
    private val okHttpClient: OkHttpClient,
    private val libraryStore: OfflineLibraryStore,
    private val settingsStorage: OfflineSettingsStorage
) {
    private val rootDir = File(context.filesDir, "offline")
    private val audioDir = File(rootDir, "audio")
    private val coversDir = File(rootDir, "covers")

    suspend fun cacheTrackAfterListening(trackId: Long) = withContext(Dispatchers.IO) {
        if (!settingsStorage.state.value.settings.saveListenedTracksEnabled) {
            libraryStore.markTrackAccessed(trackId)
            publishStorageStats()
            return@withContext
        }

        val track = libraryStore.getTrack(trackId, preferLocalMedia = false)
            ?: return@withContext
        cacheTrackMedia(track)
    }

    suspend fun cacheTrackMedia(track: TrackCard) = withContext(Dispatchers.IO) {
        cacheTrackMediaBlocking(track = track, pinAudio = false, requireAudio = false)
    }

    suspend fun downloadTrackForOffline(track: TrackCard) = withContext(Dispatchers.IO) {
        ensureManualDownloadsCanFit(listOf(track))
        cacheTrackMediaBlocking(track = track, pinAudio = true, requireAudio = true)
    }

    suspend fun downloadTracksForOffline(tracks: List<TrackCard>) = withContext(Dispatchers.IO) {
        val uniqueTracks = tracks.distinctBy { it.id }
        ensureManualDownloadsCanFit(uniqueTracks)
        uniqueTracks.forEach { track ->
            cacheTrackMediaBlocking(track = track, pinAudio = true, requireAudio = true)
        }
    }

    suspend fun validateManualDownloadSize(tracks: List<TrackCard>) = withContext(Dispatchers.IO) {
        ensureManualDownloadsCanFit(tracks.distinctBy { it.id })
    }

    private suspend fun cacheTrackMediaBlocking(track: TrackCard, pinAudio: Boolean, requireAudio: Boolean) {
        libraryStore.upsertTracks(listOf(track))
        libraryStore.markTrackAccessed(track.id)
        pruneStaleMedia()

        val storedTrack = libraryStore.getTrack(track.id, preferLocalMedia = false) ?: track
        val audioPath = storedTrack.localAudioPath
        val hadCachedAudio = !audioPath.isNullOrBlank() && File(audioPath).exists()
        val hasAudio = if (!hadCachedAudio) {
            downloadAudio(storedTrack)
        } else {
            true
        }
        if (requireAudio && !hasAudio) {
            throw IOException("Track has no downloadable audio source")
        }
        if (pinAudio && hasAudio) {
            libraryStore.markAudioPinned(storedTrack.id, pinned = true)
        }

        val coverPath = storedTrack.localCoverPath
        if ((coverPath.isNullOrBlank() || !File(coverPath).exists()) &&
            !storedTrack.remoteCoverUrl.isNullOrBlank()
        ) {
            downloadCover(storedTrack)
        }

        enforceStorageLimit()
        if (pinAudio) {
            enforceManualStorageLimit(storedTrack.id, canRemoveAudio = !hadCachedAudio)
        }
        publishStorageStats()
    }

    suspend fun clearCachedMedia() = withContext(Dispatchers.IO) {
        rootDir.deleteRecursively()
        libraryStore.clearCachedMedia()
        publishStorageStats()
    }

    suspend fun pruneStaleMedia() = withContext(Dispatchers.IO) {
        libraryStore.staleMediaPaths().forEach { stale ->
            File(stale.path).delete()
            if (stale.isAudio) {
                libraryStore.markAudioRemoved(stale.trackId)
            } else {
                libraryStore.markCoverRemoved(stale.trackId)
            }
        }
    }

    fun publishStorageStats() {
        val stats = libraryStore.storageStats()
        settingsStorage.setStorageStats(stats.usedBytes, stats.cachedTrackCount)
    }

    private fun downloadAudio(track: TrackCard): Boolean {
        val sourceUrl = track.remoteStreamUrl.ifBlank { track.streamUrl }
        if (sourceUrl.isBlank()) return false

        val extension = extensionForMime(track.audioMimeType)
            ?: extensionFromUrl(sourceUrl)
            ?: "audio"
        val target = File(audioDir, "${track.id}-${track.audioHash ?: track.audioUpdatedAt ?: "audio"}.$extension")
        val bytes = downloadToFile(sourceUrl, target)
        libraryStore.markAudioCached(track.id, target.absolutePath, bytes)
        return true
    }

    private fun downloadCover(track: TrackCard) {
        val sourceUrl = track.remoteCoverUrl ?: track.coverUrl ?: return
        if (sourceUrl.isBlank()) return

        val extension = extensionFromUrl(sourceUrl) ?: "jpg"
        val target = File(coversDir, "${track.id}-${track.coverHash ?: track.coverUpdatedAt ?: "cover"}.$extension")
        val bytes = downloadToFile(sourceUrl, target)
        libraryStore.markCoverCached(track.id, target.absolutePath, bytes)
    }

    private fun downloadToFile(url: String, target: File): Long {
        target.parentFile?.mkdirs()
        val temp = File(target.parentFile, "${target.name}.tmp")
        temp.delete()

        val request = Request.Builder()
            .url(url)
            .build()

        okHttpClient.newCall(request).execute().use { response ->
            if (!response.isSuccessful) {
                throw IOException("Offline download failed with HTTP ${response.code}")
            }
            val body = response.body ?: throw IOException("Offline download response is empty")
            temp.outputStream().use { output ->
                body.byteStream().use { input ->
                    input.copyTo(output)
                }
            }
        }

        if (!temp.renameTo(target)) {
            temp.copyTo(target, overwrite = true)
            temp.delete()
        }
        return target.length()
    }

    private fun enforceStorageLimit() {
        val limitBytes = settingsStorage.state.value.settings.storageLimitBytes
        var usedBytes = libraryStore.storageStats().usedBytes
        if (usedBytes <= limitBytes) return

        for (entry in libraryStore.cachedAudioEntriesForEviction()) {
            if (usedBytes <= limitBytes) break
            File(entry.path).delete()
            libraryStore.markAudioRemoved(entry.trackId)
            usedBytes -= entry.sizeBytes
        }
    }

    private fun ensureManualDownloadsCanFit(tracks: List<TrackCard>) {
        val limitBytes = settingsStorage.state.value.settings.storageLimitBytes
        val stats = libraryStore.storageStats()
        val evictableBytes = libraryStore.cachedAudioEntriesForEviction().sumOf { it.sizeBytes }
        val protectedUsedBytes = (stats.usedBytes - evictableBytes).coerceAtLeast(0L)
        val estimatedNeededBytes = tracks.sumOf { track ->
            val storedTrack = libraryStore.getTrack(track.id, preferLocalMedia = false)
            if (storedTrack?.isAudioCached == true) {
                0L
            } else {
                track.audioSizeBytes ?: storedTrack?.audioSizeBytes ?: 0L
            }
        }

        if (estimatedNeededBytes > 0 && protectedUsedBytes + estimatedNeededBytes > limitBytes) {
            throw IOException("Not enough offline storage limit for this download")
        }
    }

    private fun enforceManualStorageLimit(trackId: Long, canRemoveAudio: Boolean) {
        val limitBytes = settingsStorage.state.value.settings.storageLimitBytes
        enforceStorageLimit()
        if (libraryStore.storageStats().usedBytes <= limitBytes) return

        if (canRemoveAudio) {
            libraryStore.getTrack(trackId, preferLocalMedia = false)
                ?.localAudioPath
                ?.let { path -> File(path).delete() }
            libraryStore.markAudioRemoved(trackId)
        }
        throw IOException("Not enough offline storage limit for this download")
    }

    private fun extensionForMime(mimeType: String?): String? {
        return when (mimeType?.lowercase()) {
            "audio/mpeg", "audio/mp3" -> "mp3"
            "audio/flac" -> "flac"
            "audio/ogg", "audio/opus" -> "ogg"
            "audio/mp4", "audio/aac" -> "m4a"
            "audio/wav", "audio/x-wav" -> "wav"
            else -> null
        }
    }

    private fun extensionFromUrl(url: String): String? {
        val cleanUrl = url.substringBefore('?').substringBefore('#')
        return cleanUrl.substringAfterLast('/', "")
            .substringAfterLast('.', "")
            .takeIf { it.length in 2..5 && it.all(Char::isLetterOrDigit) }
            ?.lowercase()
    }
}
