/*
 * Book's Story — free and open-source Material You eBook reader.
 * Copyright (C) 2024-2026 Acclorite
 * SPDX-License-Identifier: GPL-3.0-only
 */

package org.dueattendant149.bookshelf.data.repository

import android.app.Application
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.dueattendant149.bookshelf.data.local.dto.BookEntity
import org.dueattendant149.bookshelf.data.local.dto.CachedFileEntity
import org.dueattendant149.bookshelf.data.local.room.BookDatabase
import org.dueattendant149.bookshelf.data.mapper.book.BookMapper
import org.dueattendant149.bookshelf.data.settings.ServerSettings
import org.dueattendant149.bookshelf.data.worker.CacheDownloadWorker
import org.dueattendant149.bookshelf.domain.model.cache.CacheState
import org.dueattendant149.bookshelf.domain.model.cache.CacheStatus
import org.dueattendant149.bookshelf.domain.model.cache.CachedFile
import org.dueattendant149.bookshelf.domain.model.library.Book
import org.dueattendant149.bookshelf.domain.repository.CacheRepository
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CacheRepositoryImpl
    @Inject
    constructor(
        private val database: BookDatabase,
        private val bookMapper: BookMapper,
        private val serverSettings: ServerSettings,
        private val workManager: WorkManager,
        private val application: Application,
    ) : CacheRepository {
        override suspend fun cacheBook(book: Book): Result<Unit> = runCatching {
            withContext(Dispatchers.IO) {
                val localBook = findOrInsertBook(book)
                val bookId = localBook.id
                val absUrl = serverSettings.getAbsUrl() ?: error("Audiobookshelf URL not configured")

                if (book.coverUrl.isNotBlank()) {
                    createCacheRecord(bookId, "cover", book.coverUrl)
                }
                if (book.hasEbook) {
                    createCacheRecord(
                        bookId,
                        "ebook",
                        "$absUrl/api/items/${book.remoteId}/download",
                    )
                }
                if (book.hasAudio) {
                    createCacheRecord(
                        bookId,
                        "audio",
                        "$absUrl/api/items/${book.remoteId}/download",
                    )
                }

                val inputData =
                    Data
                        .Builder()
                        .putInt(CacheDownloadWorker.KEY_BOOK_ID, bookId)
                        .putString(CacheDownloadWorker.KEY_REMOTE_ID, book.remoteId)
                        .putString(CacheDownloadWorker.KEY_TITLE, book.title)
                        .putString(CacheDownloadWorker.KEY_MEDIA_TYPE, book.mediaType)
                        .putBoolean(CacheDownloadWorker.KEY_HAS_AUDIO, book.hasAudio)
                        .putBoolean(CacheDownloadWorker.KEY_HAS_EBOOK, book.hasEbook)
                        .putString(CacheDownloadWorker.KEY_COVER_URL, book.coverUrl)
                        .build()

                val request =
                    OneTimeWorkRequestBuilder<CacheDownloadWorker>()
                        .setInputData(inputData)
                        .setConstraints(
                            Constraints
                                .Builder()
                                .setRequiredNetworkType(NetworkType.CONNECTED)
                                .build(),
                        )
                        .build()

                workManager.enqueueUniqueWork(
                    CacheDownloadWorker.workName(bookId),
                    ExistingWorkPolicy.KEEP,
                    request,
                )
            }
        }

        override suspend fun deleteCache(remoteId: String): Result<Unit> = runCatching {
            withContext(Dispatchers.IO) {
                val book = database.bookDao.findBookByRemoteId(remoteId) ?: return@withContext
                val records = database.cachedFileDao.getByBookId(book.id)
                records.forEach { record ->
                    record.localPath?.let { path ->
                        File(path).delete()
                    }
                    database.cachedFileDao.delete(record)
                }
                workManager.cancelUniqueWork(CacheDownloadWorker.workName(book.id))
            }
        }

        override fun observeCacheStatus(remoteId: String): Flow<CacheStatus> =
            flow {
                val bookId = database.bookDao.findBookByRemoteId(remoteId)?.id
                if (bookId == null) {
                    emit(
                        CacheStatus(
                            remoteId = remoteId,
                            state = CacheState.NONE,
                            progress = 0f,
                            cachedFiles = emptyList(),
                        ),
                    )
                    return@flow
                }

                database.cachedFileDao.observeByBookId(bookId).map { records ->
                    records.toCacheStatus(remoteId)
                }.collect { emit(it) }
            }.flowOn(Dispatchers.IO)

        private suspend fun findOrInsertBook(book: Book): BookEntity {
            val existing = database.bookDao.findBookByRemoteId(book.remoteId)
            return if (existing != null) {
                val updated =
                    existing.copy(
                        title = book.title,
                        author = book.author.getAsString() ?: "",
                        coverUrl = book.coverUrl,
                        hasAudio = book.hasAudio,
                        hasEbook = book.hasEbook,
                        audioDuration = book.audioDuration,
                        mediaType = book.mediaType,
                        libraryId = book.libraryId,
                        lastSyncedAt = System.currentTimeMillis(),
                    )
                database.bookDao.updateBook(updated)
                database.bookDao.findBookByRemoteId(book.remoteId) ?: updated
            } else {
                val entity = bookMapper.toBookEntity(book.copy(filePath = ""))
                database.bookDao.insertBook(entity)
                database.bookDao.findBookByRemoteId(book.remoteId)
                    ?: error("Failed to insert book for caching")
            }
        }

        private suspend fun createCacheRecord(
            bookId: Int,
            type: String,
            url: String,
        ) {
            val existing = database.cachedFileDao.getByBookIdAndTypeSingle(bookId, type)
            if (existing == null) {
                database.cachedFileDao.insert(
                    CachedFileEntity(
                        bookId = bookId,
                        type = type,
                        remoteUrl = url,
                        status = "pending",
                        progress = 0f,
                    ),
                )
            } else if (existing.status == "completed" &&
                existing.localPath != null &&
                File(existing.localPath).exists()
            ) {
                // Already cached locally, leave it alone.
            } else {
                database.cachedFileDao.update(
                    existing.copy(status = "pending", progress = 0f),
                )
            }
        }

        private fun List<CachedFileEntity>.toCacheStatus(remoteId: String): CacheStatus {
            if (isEmpty()) {
                return CacheStatus(
                    remoteId = remoteId,
                    state = CacheState.NONE,
                    progress = 0f,
                    cachedFiles = emptyList(),
                )
            }

            val files =
                map {
                    CachedFile(
                        type = it.type,
                        status = it.status,
                        progress = it.progress,
                        localPath = it.localPath,
                    )
                }

            val state =
                when {
                    files.any { it.status == "failed" } -> CacheState.FAILED
                    files.any { it.status == "downloading" } -> CacheState.DOWNLOADING
                    files.all { it.status == "completed" } -> CacheState.COMPLETED
                    else -> CacheState.PENDING
                }

            val progress =
                when (state) {
                    CacheState.DOWNLOADING -> files.filter { it.status == "downloading" }.map { it.progress }.average().toFloat()
                    CacheState.COMPLETED -> 1f
                    else -> 0f
                }

            return CacheStatus(
                remoteId = remoteId,
                state = state,
                progress = progress,
                cachedFiles = files,
            )
        }
    }
