package com.yorvana.testsupport

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import com.yorvana.data.model.ServiceRecord
import com.yorvana.data.model.Vehicle
import com.yorvana.util.DateFormatter
import com.yorvana.util.UuidGenerator
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import java.io.File
import java.io.InputStream
import java.io.OutputStream

class VaultFixture private constructor(
    private val context: Context,
    val uri: Uri,
) {
    val root: DocumentFile =
        if (uri.scheme == "file") {
            DocumentFile.fromFile(File(uri.path ?: error("Missing file path: $uri")))
        } else {
            DocumentFile.fromTreeUri(context, uri) ?: error("Unable to open tree URI: $uri")
        }

    fun seedSampleVault(
        vehicleId: String = UuidGenerator.generate(),
        recordId: String = UuidGenerator.generate(),
        vehicleName: String = "Fixture Car",
        attachmentName: String = "receipt.txt",
        attachmentBytes: ByteArray = "fixture attachment".toByteArray(),
    ): SeededVault {
        val vehicle =
            Vehicle(
                id = vehicleId,
                nickname = vehicleName,
                createdAt = DateFormatter.nowIso(),
                lastServiceDate = "2025-01-01",
                recordCount = 1,
            )
        val record =
            ServiceRecord(
                id = recordId,
                vehicleId = vehicleId,
                date = "2025-01-01",
                odometer = 12345,
                category = "oil-change",
                performer = "Self",
                attachments = listOf(attachmentName),
                createdAt = DateFormatter.nowIso(),
                updatedAt = DateFormatter.nowIso(),
            )

        writeText("vehicles/$vehicleId/vehicle.json", json.encodeToString(Vehicle.serializer(), vehicle))
        writeText("vehicles/$vehicleId/records/$recordId.json", json.encodeToString(ServiceRecord.serializer(), record))
        writeBytes("vehicles/$vehicleId/records/$recordId/$attachmentName", attachmentBytes)
        return SeededVault(vehicle, record, attachmentName, attachmentBytes)
    }

    fun writeMalformedVehicle(vehicleId: String) {
        writeText("vehicles/$vehicleId/vehicle.json", "{not valid json")
    }

    fun writeText(
        relativePath: String,
        contents: String,
    ) {
        writeBytes(relativePath, contents.toByteArray())
    }

    fun writeBytes(
        relativePath: String,
        bytes: ByteArray,
    ) {
        openOutputStream(relativePath).use { it.write(bytes) }
    }

    private fun openOutputStream(relativePath: String): OutputStream {
        if (uri.scheme == "file") {
            val file = File(uri.path!!, relativePath)
            file.parentFile?.mkdirs()
            android.util.Log.d("VaultFixture", "Writing local file: ${file.absolutePath}")
            return file.outputStream()
        } else {
            val file = fileFor(relativePath, create = true) ?: error("Unable to create $relativePath")
            android.util.Log.d("VaultFixture", "Writing remote file: ${file.uri}")
            return context.contentResolver.openOutputStream(file.uri, "rwt") ?: error("Unable to open output stream for ${file.uri}")
        }
    }

    private fun openInputStream(relativePath: String): InputStream =
        if (uri.scheme == "file") {
            File(uri.path!!, relativePath).inputStream()
        } else {
            context.contentResolver.openInputStream(
                fileFor(relativePath, create = false)?.uri ?: error("Missing $relativePath"),
            ) ?: error("Unable to read $relativePath")
        }

    fun exists(relativePath: String): Boolean =
        if (uri.scheme == "file") {
            File(uri.path!!, relativePath).exists()
        } else {
            fileFor(relativePath, create = false)?.exists() == true
        }

    fun readBytes(relativePath: String): ByteArray = openInputStream(relativePath).use { it.readBytes() }

    fun snapshot(): Map<String, ByteArray> {
        val files = linkedMapOf<String, ByteArray>()
        collectFilesRecursive(root, "", files)
        return files
    }

    fun assertSnapshotEquals(expected: Map<String, ByteArray>) {
        val actual = snapshot()
        assertThat(actual.keys).containsExactlyElementsIn(expected.keys)
        expected.forEach { (path, bytes) ->
            assertThat(actual[path]).isEqualTo(bytes)
        }
    }

    fun isEmpty(): Boolean = root.listFiles().isEmpty()

    fun deleteContents() {
        root.listFiles().forEach { it.deleteRecursively() }
    }

    private fun collectFilesRecursive(
        dir: DocumentFile,
        prefix: String,
        files: MutableMap<String, ByteArray>,
    ) {
        dir.listFiles().forEach { child ->
            val name = child.name ?: return@forEach
            val path = if (prefix.isBlank()) name else "$prefix/$name"
            if (child.isDirectory) {
                collectFilesRecursive(child, path, files)
            } else {
                files[path] = readBytes(path)
            }
        }
    }

    private fun fileFor(
        relativePath: String,
        create: Boolean,
    ): DocumentFile? {
        val parts = relativePath.split("/").filter { it.isNotBlank() }
        var current = root
        parts.dropLast(1).forEach { segment ->
            val next =
                current.findFile(segment)
                    ?: if (create) {
                        current.createDirectory(segment)
                    } else {
                        return null
                    }
            current = next ?: return null
        }
        val filename = parts.last()
        return current.findFile(filename)
            ?: if (create) current.createFile(mimeTypeFor(filename), filename) else null
    }

    private fun DocumentFile.deleteRecursively() {
        if (isDirectory) listFiles().forEach { it.deleteRecursively() }
        delete()
    }

    companion object {
        private val json =
            Json {
                ignoreUnknownKeys = true
                encodeDefaults = true
            }

        fun createLocal(name: String = "yorvana-vault-fixture-${UuidGenerator.generate()}"): VaultFixture {
            val context = InstrumentationRegistry.getInstrumentation().targetContext
            val externalFilesDir = context.getExternalFilesDir(null) ?: error("External files dir unavailable")
            val rootDir =
                File(externalFilesDir, name).apply {
                    if (exists()) deleteRecursively()
                    mkdirs()
                }
            return VaultFixture(context, Uri.fromFile(rootDir))
        }

        fun useExisting(uri: Uri): VaultFixture {
            val context = InstrumentationRegistry.getInstrumentation().targetContext
            takePersistablePermission(context, uri)
            return VaultFixture(context, uri)
        }

        fun active(): VaultFixture {
            val context = InstrumentationRegistry.getInstrumentation().targetContext
            val uriString =
                runBlocking {
                    (context.applicationContext as com.yorvana.YorvanaApplication)
                        .preferences
                        .preferences
                        .first()
                        .vaultUriString
                } ?: error("No active vault URI")
            return useExisting(Uri.parse(uriString))
        }

        private fun takePersistablePermission(
            context: Context,
            uri: Uri,
        ) {
            if (uri.scheme == "file") return
            runCatching {
                context.contentResolver.takePersistableUriPermission(
                    uri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
                )
            }
        }

        private fun mimeTypeFor(filename: String): String =
            when (filename.substringAfterLast('.', "").lowercase()) {
                "json" -> "application/json"
                "pdf" -> "application/pdf"
                "jpg", "jpeg" -> "image/jpeg"
                "png" -> "image/png"
                "txt" -> "text/plain"
                else -> "application/octet-stream"
            }
    }
}

data class SeededVault(
    val vehicle: Vehicle,
    val record: ServiceRecord,
    val attachmentName: String,
    val attachmentBytes: ByteArray,
)
