package com.syncflow.domain.sync

import android.content.ContentResolver
import android.net.Uri
import android.provider.DocumentsContract
import com.syncflow.domain.model.SyncPair
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream

data class LocalFileInfo(val relativePath: String, val sizeBytes: Long, val lastModifiedMs: Long)

sealed class LocalAccessor {

    abstract fun walkFiles(pair: SyncPair): Map<String, LocalFileInfo>
    abstract fun openInputStream(relativePath: String): InputStream?
    abstract fun delete(relativePath: String): Boolean
    abstract fun lastModifiedMs(relativePath: String): Long

    /**
     * Write [relativePath] atomically: stream into a temp sibling first, then swap it into
     * place only after [write] completes without throwing. An interrupted transfer (network
     * drop, process death) leaves the existing destination untouched instead of truncating it.
     * On failure the temp is removed and the exception is rethrown.
     */
    abstract suspend fun writeAtomically(relativePath: String, write: suspend (OutputStream) -> Unit)

    // ── java.io.File backend (regular /storage/... paths) ────────────────────

    class JavaFile(private val root: File) : LocalAccessor() {

        override fun walkFiles(pair: SyncPair): Map<String, LocalFileInfo> {
            if (!root.exists()) return emptyMap()
            val includeExts = pair.includeExtensions.map { it.lowercase().trimStart('.') }.toSet()
            val excludeExts = pair.excludeExtensions.map { it.lowercase().trimStart('.') }.toSet()
            val minBytes = pair.minFileSizeKb * 1024L
            val maxBytes = if (pair.maxFileSizeKb > 0) pair.maxFileSizeKb * 1024L else Long.MAX_VALUE

            return root.walkTopDown()
                .onEnter { dir -> pair.recursive || dir == root }
                .filter { it.isFile }
                .filter { f -> !pair.skipHiddenFiles || !f.name.startsWith('.') }
                .filter { f -> pair.excludePatterns.none { pat -> f.name.matches(globToRegex(pat)) } }
                .filter { f ->
                    val ext = f.extension.lowercase()
                    (includeExts.isEmpty() || ext in includeExts) && ext !in excludeExts
                }
                .filter { f -> f.length() in minBytes..maxBytes }
                .associate { f ->
                    val rel = f.relativeTo(root).path
                    rel to LocalFileInfo(rel, f.length(), f.lastModified())
                }
        }

        override fun openInputStream(relativePath: String): InputStream =
            File(root, relativePath).inputStream()

        override suspend fun writeAtomically(relativePath: String, write: suspend (OutputStream) -> Unit) {
            val dest = File(root, relativePath)
            dest.parentFile?.mkdirs()
            val tmp = File(dest.parentFile, ".${dest.name}.sfpart")
            try {
                FileOutputStream(tmp).use { os ->
                    write(os)
                    os.flush()
                    os.fd.sync() // durably persist bytes before the rename swaps the file in
                }
            } catch (e: Throwable) {
                tmp.delete()
                throw e
            }
            // Same-directory rename is atomic on POSIX/Android and replaces the destination.
            if (!tmp.renameTo(dest)) {
                try {
                    tmp.copyTo(dest, overwrite = true)
                } catch (e: Throwable) {
                    tmp.delete()
                    throw e
                }
                tmp.delete()
            }
        }

        override fun delete(relativePath: String): Boolean = File(root, relativePath).delete()

        override fun lastModifiedMs(relativePath: String): Long = File(root, relativePath).lastModified()
    }

    // ── SAF backend (content:// tree URIs from Storage Access Framework) ─────

    class Saf(private val treeUri: Uri, private val resolver: ContentResolver) : LocalAccessor() {

        // Populated by walkFiles so openInputStream can skip the re-query for files that
        // already exist locally (uploads). Root-level files are the common failure case
        // when findDocUri re-queries: the cache sidesteps the issue entirely.
        private val docIdCache = mutableMapOf<String, String>()

        override fun walkFiles(pair: SyncPair): Map<String, LocalFileInfo> {
            docIdCache.clear()
            val rootDocId = DocumentsContract.getTreeDocumentId(treeUri)
            val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, rootDocId)
            return cursorWalk(childrenUri, "", pair)
        }

        private fun cursorWalk(childrenUri: Uri, base: String, pair: SyncPair): Map<String, LocalFileInfo> {
            val result = mutableMapOf<String, LocalFileInfo>()
            val includeExts = pair.includeExtensions.map { it.lowercase().trimStart('.') }.toSet()
            val excludeExts = pair.excludeExtensions.map { it.lowercase().trimStart('.') }.toSet()
            val minBytes = pair.minFileSizeKb * 1024L
            val maxBytes = if (pair.maxFileSizeKb > 0) pair.maxFileSizeKb * 1024L else Long.MAX_VALUE

            val cursor = resolver.query(
                childrenUri,
                arrayOf(
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                    DocumentsContract.Document.COLUMN_MIME_TYPE,
                    DocumentsContract.Document.COLUMN_SIZE,
                    DocumentsContract.Document.COLUMN_LAST_MODIFIED,
                ),
                null, null, null,
            ) ?: return result

            cursor.use {
                while (it.moveToNext()) {
                    val docId = it.getString(0) ?: continue
                    val name = it.getString(1) ?: continue
                    val mime = it.getString(2) ?: continue
                    val size = it.getLong(3)
                    val modified = it.getLong(4)
                    val rel = if (base.isEmpty()) name else "$base/$name"

                    if (mime == DocumentsContract.Document.MIME_TYPE_DIR) {
                        if (pair.recursive) {
                            val subUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, docId)
                            result.putAll(cursorWalk(subUri, rel, pair))
                        }
                    } else {
                        if (pair.skipHiddenFiles && name.startsWith('.')) continue
                        if (pair.excludePatterns.any { pat -> name.matches(globToRegex(pat)) }) continue
                        val ext = name.substringAfterLast('.', "").lowercase()
                        if (includeExts.isNotEmpty() && ext !in includeExts) continue
                        if (ext in excludeExts) continue
                        if (size !in minBytes..maxBytes) continue
                        result[rel] = LocalFileInfo(rel, size, modified)
                        docIdCache[rel] = docId
                    }
                }
            }
            return result
        }

        override fun openInputStream(relativePath: String): InputStream? {
            val docUri = docIdCache[relativePath]
                ?.let { DocumentsContract.buildDocumentUriUsingTree(treeUri, it) }
                ?: findDocUri(relativePath)
                ?: return null
            return resolver.openInputStream(docUri)
        }

        override suspend fun writeAtomically(relativePath: String, write: suspend (OutputStream) -> Unit) {
            val parts = relativePath.replace('\\', '/').split('/')
            var currentId = DocumentsContract.getTreeDocumentId(treeUri)

            for (i in 0 until parts.size - 1) {
                val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, currentId)
                currentId = findChildId(childrenUri, parts[i]) ?: run {
                    val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, currentId)
                    val newDir = DocumentsContract.createDocument(
                        resolver, parentUri, DocumentsContract.Document.MIME_TYPE_DIR, parts[i]
                    ) ?: throw IOException("Cannot create directory ${parts[i]} for $relativePath")
                    DocumentsContract.getDocumentId(newDir)
                }
            }

            val fileName = parts.last()
            val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, currentId)
            val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, currentId)
            val tmpName = ".$fileName.sfpart"

            // Clear any leftover temp document from a previously interrupted write.
            findChildId(childrenUri, tmpName)?.let { staleId ->
                runCatching {
                    DocumentsContract.deleteDocument(
                        resolver, DocumentsContract.buildDocumentUriUsingTree(treeUri, staleId)
                    )
                }
            }

            val tmpUri = DocumentsContract.createDocument(
                resolver, parentUri, "application/octet-stream", tmpName
            ) ?: throw IOException("Cannot create temp document for $relativePath")

            try {
                (resolver.openOutputStream(tmpUri)
                    ?: throw IOException("Cannot open temp stream for $relativePath")).use { os ->
                    write(os)
                    os.flush()
                }
            } catch (e: Throwable) {
                runCatching { DocumentsContract.deleteDocument(resolver, tmpUri) }
                throw e
            }

            // Commit: remove the existing destination, then rename the fully-written temp into
            // place. If interrupted between the two steps the temp still holds the complete data
            // (recoverable by hand), which is strictly safer than truncating the destination.
            findChildId(childrenUri, fileName)?.let { existingId ->
                DocumentsContract.deleteDocument(
                    resolver, DocumentsContract.buildDocumentUriUsingTree(treeUri, existingId)
                )
            }
            val renamed = DocumentsContract.renameDocument(resolver, tmpUri, fileName)
            if (renamed == null) {
                runCatching { DocumentsContract.deleteDocument(resolver, tmpUri) }
                throw IOException("Cannot finalize $relativePath")
            }
            // Drop the stale cache entry so the next read re-resolves the new document id.
            docIdCache.remove(relativePath)
        }

        override fun delete(relativePath: String): Boolean {
            val docUri = docIdCache[relativePath]
                ?.let { DocumentsContract.buildDocumentUriUsingTree(treeUri, it) }
                ?: findDocUri(relativePath)
                ?: return false
            return try {
                DocumentsContract.deleteDocument(resolver, docUri)
            } catch (e: Exception) {
                false
            }
        }

        override fun lastModifiedMs(relativePath: String): Long {
            val docUri = findDocUri(relativePath) ?: return 0L
            val cursor = resolver.query(
                docUri,
                arrayOf(DocumentsContract.Document.COLUMN_LAST_MODIFIED),
                null, null, null,
            ) ?: return 0L
            return cursor.use { if (it.moveToFirst()) it.getLong(0) else 0L }
        }

        private fun findDocUri(relativePath: String): Uri? {
            var currentId = DocumentsContract.getTreeDocumentId(treeUri)
            for (part in relativePath.replace('\\', '/').split('/')) {
                val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, currentId)
                currentId = findChildId(childrenUri, part) ?: return null
            }
            return DocumentsContract.buildDocumentUriUsingTree(treeUri, currentId)
        }

        private fun findChildId(childrenUri: Uri, name: String): String? {
            val cursor = resolver.query(
                childrenUri,
                arrayOf(
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                ),
                null, null, null,
            ) ?: return null
            return cursor.use {
                while (it.moveToNext()) {
                    if (it.getString(1) == name) return@use it.getString(0)
                }
                null
            }
        }
    }
}

internal fun globToRegex(pat: String): Regex =
    Regex(pat.replace(".", "\\.").replace("*", ".*").replace("?", "."), RegexOption.IGNORE_CASE)
