package eu.geyskens.pdfscan

import android.app.Application
import android.graphics.Bitmap
import android.graphics.PointF
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import eu.geyskens.pdfscan.data.AppSettings
import eu.geyskens.pdfscan.data.Page
import eu.geyskens.pdfscan.data.ScanFilter
import eu.geyskens.pdfscan.data.SettingsRepository
import eu.geyskens.pdfscan.paperless.PaperlessClient
import eu.geyskens.pdfscan.pdf.PageSizeMode
import eu.geyskens.pdfscan.pdf.PdfBuilder
import eu.geyskens.pdfscan.scan.BitmapIo
import eu.geyskens.pdfscan.scan.EdgeDetector
import eu.geyskens.pdfscan.scan.ImageProcessor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import java.io.File

/** Owns the in-progress scan session: the list of pages, their edits, and export. */
class ScanViewModel(app: Application) : AndroidViewModel(app) {

    private val settingsRepo = SettingsRepository(app)
    val settings: StateFlow<AppSettings> =
        settingsRepo.settings.stateIn(viewModelScope, SharingStarted.Eagerly, AppSettings())

    private val _pages = MutableStateFlow<List<Page>>(emptyList())
    val pages: StateFlow<List<Page>> = _pages.asStateFlow()

    private val capturesDir = File(app.cacheDir, "captures").apply { mkdirs() }
    private val exportsDir = File(app.cacheDir, "exports").apply { mkdirs() }

    // Rendered-page cache keyed by page id, invalidated when the page's edit hash changes.
    private val renderCache = HashMap<String, Pair<Int, Bitmap>>()

    fun capturesDir(): File = capturesDir

    /** Called after CameraX writes a capture; runs edge detection and appends a page. */
    suspend fun onImageCaptured(file: File) = withContext(Dispatchers.Default) {
        val bitmap = BitmapIo.loadOriented(file)
        val corners = EdgeDetector.detect(bitmap) ?: Page.fullFrameCorners()
        bitmap.recycle()
        val page = Page(
            sourceFile = file,
            corners = corners,
            filter = settings.value.defaultFilter,
        )
        _pages.value = _pages.value + page
    }

    fun page(id: String): Page? = _pages.value.firstOrNull { it.id == id }

    /** Loads the raw, EXIF-oriented capture (not perspective-corrected) for the crop overlay. */
    suspend fun loadSource(page: Page): Bitmap =
        withContext(Dispatchers.Default) { BitmapIo.loadOriented(page.sourceFile) }

    fun updateCorners(id: String, corners: List<PointF>) = mutate(id) { it.copy(corners = corners) }
    fun setFilter(id: String, filter: ScanFilter) = mutate(id) { it.copy(filter = filter) }
    fun rotate(id: String) = mutate(id) { it.copy(rotationDegrees = (it.rotationDegrees + 90) % 360) }
    fun resetCorners(id: String) = mutate(id) { it.copy(corners = Page.fullFrameCorners()) }

    fun delete(id: String) {
        _pages.value.firstOrNull { it.id == id }?.sourceFile?.delete()
        // Don't recycle here: a leaving composition may still be drawing this bitmap.
        renderCache.remove(id)
        _pages.value = _pages.value.filterNot { it.id == id }
    }

    fun move(from: Int, to: Int) {
        val list = _pages.value.toMutableList()
        if (from in list.indices && to in list.indices) {
            list.add(to, list.removeAt(from))
            _pages.value = list
        }
    }

    private inline fun mutate(id: String, transform: (Page) -> Page) {
        _pages.value = _pages.value.map { if (it.id == id) transform(it) else it }
        // Drop the stale render but let GC reclaim it; recycling now can hit a live draw.
        renderCache.remove(id)
    }

    /** Renders a page to a deskewed, filtered bitmap. Cached until the page is edited. */
    suspend fun render(page: Page): Bitmap = withContext(Dispatchers.Default) {
        val hash = editHash(page)
        renderCache[page.id]?.let { (h, bmp) -> if (h == hash && !bmp.isRecycled) return@withContext bmp }
        val source = BitmapIo.loadOriented(page.sourceFile)
        val result = ImageProcessor.process(source, page.corners, page.rotationDegrees, page.filter)
        source.recycle()
        renderCache[page.id] = hash to result
        result
    }

    private fun editHash(page: Page): Int =
        listOf(page.corners, page.rotationDegrees, page.filter, page.sourceFile.path).hashCode()

    /** Builds the multi-page PDF into the shareable exports dir and returns it. */
    suspend fun exportPdf(fileName: String, mode: PageSizeMode = PageSizeMode.A4): File =
        withContext(Dispatchers.Default) {
            val safe = fileName.ifBlank { "scan" }.replace(Regex("[^A-Za-z0-9-_ ]"), "_")
            val rendered = _pages.value.map { render(it) }
            val out = File(exportsDir, "$safe.pdf")
            PdfBuilder.build(rendered, out, mode)
            out
        }

    suspend fun uploadToPaperless(pdf: File, title: String): PaperlessClient.Result {
        val s = settings.value
        val token = settingsRepo.getToken()
        if (s.paperlessUrl.isBlank() || token.isNullOrBlank()) {
            return PaperlessClient.Result.Failure(
                getApplication<Application>().getString(R.string.err_paperless_not_setup)
            )
        }
        return PaperlessClient(s.paperlessUrl, token, getApplication<Application>()).upload(pdf, title)
    }

    // --- Settings pass-throughs (SettingsScreen) ---

    fun currentToken(): String? = settingsRepo.getToken()

    suspend fun savePaperless(url: String, token: String) {
        settingsRepo.setPaperlessUrl(url.trim())
        settingsRepo.setToken(token.trim())
    }

    suspend fun setDefaultFilter(filter: ScanFilter) = settingsRepo.setDefaultFilter(filter)

    suspend fun testPaperless(url: String, token: String): PaperlessClient.Result {
        if (url.isBlank() || token.isBlank()) {
            return PaperlessClient.Result.Failure(
                getApplication<Application>().getString(R.string.err_enter_url_token)
            )
        }
        return PaperlessClient(url.trim(), token.trim(), getApplication<Application>()).testConnection()
    }

    fun clearSession() {
        _pages.value.forEach { it.sourceFile.delete() }
        renderCache.values.forEach { it.second.recycle() }
        renderCache.clear()
        _pages.value = emptyList()
    }
}
