/*
 * 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.worker

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import okhttp3.ResponseBody
import org.dueattendant149.bookshelf.data.local.dto.CachedFileEntity
import org.dueattendant149.bookshelf.data.local.room.BookDatabase
import org.dueattendant149.bookshelf.data.remote.audiobookshelf.AudiobookshelfApiClientFactory
import org.dueattendant149.bookshelf.data.settings.ServerSettings
import retrofit2.Response
import java.io.File
import java.io.IOException

class CacheDownloadWorker(
    context: Context,
    params: WorkerParameters,
) : CoroutineWorker(context, params) {
    companion object {
        const val KEY_BOOK_ID = "book_id"
        const val KEY_REMOTE_ID = "remote_id"
        const val KEY_TITLE = "title"
        const val KEY_MEDIA_TYPE = "media_type"
        const val KEY_HAS_AUDIO = "has_audio"
        const val KEY_HAS_EBOOK = "has_ebook"
        const val KEY_COVER_URL = "cover_url"

        fun workName(bookId: Int) = "cache-download-$bookId"
    }

    private val entryPoint: CacheWorkerEntryPoint by lazy {
        EntryPointAccessors.fromApplication(applicationContext, CacheWorkerEntryPoint::class.java)
    }

    override suspend fun doWork(): Result {
        val bookId = inputData.getInt(KEY_BOOK_ID, -1)
        val remoteId = inputData.getString(KEY_REMOTE_ID) ?: return Result.failure()
        val title = inputData.getString(KEY_TITLE) ?: remoteId
        val hasAudio = inputData.getBoolean(KEY_HAS_AUDIO, false)
        val hasEbook = inputData.getBoolean(KEY_HAS_EBOOK, false)
        val coverUrl = inputData.getString(KEY_COVER_URL) ?: ""

        if (bookId < 0) {
            return Result.failure()
        }

        val database = entryPoint.database()
        val serverSettings = entryPoint.serverSettings()
        val clientFactory = entryPoint.audiobookshelfFactory()

        val absUrl = serverSettings.getAbsUrl()
        val token = serverSettings.getAbsToken()
        if (absUrl.isNullOrBlank() || token.isNullOrBlank()) {
            Log.e(TAG, "ABS credentials not configured")
            return Result.failure()
        }

        val client = clientFactory.provideClient(absUrl, token)
        if (client == null) {
            Log.e(TAG, "Failed to create ABS client")
            return Result.failure()
        }

        val bookDir = File(applicationContext.filesDir, "bookshelf-cache/$bookId").apply { mkdirs() }

        return try {
            if (coverUrl.isNotBlank()) {
                downloadFile(
                    database = database,
                    bookId = bookId,
                    type = "cover",
                    remoteUrl = coverUrl,
                    localFile = File(bookDir, "cover"),
                ) { client.getCover(remoteId) }
            }

            if (hasEbook) {
                downloadFile(
                    database = database,
                    bookId = bookId,
                    type = "ebook",
                    remoteUrl = "$absUrl/api/items/$remoteId/download",
                    localFile = File(bookDir, "ebook"),
                ) { client.downloadBook(remoteId) }
            }

            if (hasAudio) {
                downloadFile(
                    database = database,
                    bookId = bookId,
                    type = "audio",
                    remoteUrl = "$absUrl/api/items/$remoteId/download",
                    localFile = File(bookDir, "audio"),
                ) { client.downloadBook(remoteId) }
            }

            Result.success()
        } catch (e: Exception) {
            Log.e(TAG, "Cache download failed for book $bookId ($title)", e)
            Result.retry()
        }
    }

    private suspend fun downloadFile(
        database: BookDatabase,
        bookId: Int,
        type: String,
        remoteUrl: String,
        localFile: File,
        download: suspend () -> Response<ResponseBody>,
    ) {
        val record = getOrCreateRecord(database, bookId, type, remoteUrl)

        if (record.status == "completed" && record.localPath != null && File(record.localPath).exists()) {
            return
        }

        database.cachedFileDao.update(record.copy(status = "downloading", progress = 0f))

        try {
            val response = download()
            if (!response.isSuccessful) {
                throw IOException("Download failed: ${response.code()}")
            }

            val body = response.body() ?: throw IOException("Empty response body")
            val totalBytes = body.contentLength().takeIf { it > 0 } ?: -1L

            body.byteStream().use { input ->
                localFile.outputStream().use { output ->
                    val buffer = ByteArray(8192)
                    var read: Int
                    var totalRead = 0L
                    while (input.read(buffer).also { read = it } != -1) {
                        output.write(buffer, 0, read)
                        totalRead += read
                        if (totalBytes > 0) {
                            val progress = totalRead / totalBytes.toFloat()
                            database.cachedFileDao.update(
                                record.copy(status = "downloading", progress = progress),
                            )
                        }
                    }
                }
            }

            database.cachedFileDao.update(
                record.copy(
                    status = "completed",
                    progress = 1f,
                    localPath = localFile.absolutePath,
                ),
            )
        } catch (e: Exception) {
            database.cachedFileDao.update(record.copy(status = "failed", progress = 0f))
            throw e
        }
    }

    private suspend fun getOrCreateRecord(
        database: BookDatabase,
        bookId: Int,
        type: String,
        remoteUrl: String,
    ): CachedFileEntity {
        return database.cachedFileDao.getByBookIdAndTypeSingle(bookId, type)
            ?: CachedFileEntity(
                bookId = bookId,
                type = type,
                remoteUrl = remoteUrl,
            ).also { database.cachedFileDao.insert(it) }
    }

    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface CacheWorkerEntryPoint {
        fun database(): BookDatabase
        fun serverSettings(): ServerSettings
        fun audiobookshelfFactory(): AudiobookshelfApiClientFactory
    }
}

private const val TAG = "CacheDownloadWorker"
