package com.seven.ikar.kotlin.feature.archive

import android.content.Context
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.text.Normalizer
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.zip.ZipInputStream
import kotlin.coroutines.coroutineContext

data class ArchiveEntryPreview(
    val name: String,
    val isDirectory: Boolean,
    val sizeBytes: Long?
)

data class ArchiveInspectionResult(
    val totalEntries: Int,
    val totalFiles: Int,
    val totalDirectories: Int,
    val knownUncompressedBytes: Long,
    val skippedUnsafeEntries: Int,
    val previewEntries: List<ArchiveEntryPreview>
)

data class ArchiveExtractionOptions(
    val keepFolders: Boolean = true,
    val overwriteExisting: Boolean = false
)

data class ArchiveExtractionProgress(
    val processedEntries: Int,
    val totalEntries: Int,
    val extractedFiles: Int,
    val writtenBytes: Long,
    val currentEntry: String?
)

data class ArchiveExtractionResult(
    val outputDirectoryPath: String,
    val processedEntries: Int,
    val extractedFiles: Int,
    val createdDirectories: Int,
    val writtenBytes: Long,
    val skippedUnsafeEntries: Int
)

class AndroidArchiveFileService(
    private val context: Context
) {
    private val maxArchiveImportBytes = 512L * 1024L * 1024L
    private val archiveImportsDirectory: File by lazy {
        File(context.cacheDir, "archive_imports").apply { mkdirs() }
    }

    suspend fun importFromUri(uri: Uri): PendingArchiveFile = withContext(Dispatchers.IO) {
        val resolver = context.contentResolver
        val contentType = resolver.getType(uri) ?: "application/octet-stream"
        val fileName = queryDisplayName(uri)?.takeIf { it.isNotBlank() }
            ?: uri.lastPathSegment?.substringAfterLast('/')
            ?: "archive.zip"
        if (!isSupportedZip(fileName, contentType)) {
            throw IOException("Поддерживается только ZIP-архив. Этот файл не похож на ZIP.")
        }

        val declaredSize = queryFileSize(uri)
        if (declaredSize != null && declaredSize > maxArchiveImportBytes) {
            throw IOException("Архив слишком большой: максимум 512 МБ.")
        }

        val target = File(
            archiveImportsDirectory,
            "open_${System.currentTimeMillis()}_${sanitizeFileName(fileName)}"
        )
        val copiedBytes = try {
            resolver.openInputStream(uri)?.use { input -> copyToFileWithLimit(input, target) }
                ?: throw IOException("Android не дал доступ к выбранному архиву.")
        } catch (error: Exception) {
            target.delete()
            throw error
        }

        PendingArchiveFile(
            fileName = fileName,
            contentType = contentType,
            fileSizeBytes = copiedBytes,
            localPath = target.absolutePath
        )
    }

    suspend fun inspectZip(archive: PendingArchiveFile): ArchiveInspectionResult = withContext(Dispatchers.IO) {
        requireZipFile(archive).inputStream().use { input ->
            ZipInputStream(input).use { zip ->
                var totalEntries = 0
                var totalFiles = 0
                var totalDirectories = 0
                var knownUncompressedBytes = 0L
                var skippedUnsafeEntries = 0
                val previewEntries = mutableListOf<ArchiveEntryPreview>()

                while (true) {
                    coroutineContext.ensureActive()
                    val entry = zip.nextEntry ?: break
                    totalEntries += 1
                    val safeName = safeRelativePath(entry.name)
                    if (safeName == null) {
                        skippedUnsafeEntries += 1
                        zip.closeEntry()
                        continue
                    }

                    if (entry.isDirectory) {
                        totalDirectories += 1
                    } else {
                        totalFiles += 1
                        if (entry.size > 0) {
                            knownUncompressedBytes += entry.size
                        }
                    }

                    if (previewEntries.size < MaxPreviewEntries) {
                        previewEntries += ArchiveEntryPreview(
                            name = safeName,
                            isDirectory = entry.isDirectory,
                            sizeBytes = entry.size.takeIf { it >= 0L }
                        )
                    }
                    zip.closeEntry()
                }

                ArchiveInspectionResult(
                    totalEntries = totalEntries,
                    totalFiles = totalFiles,
                    totalDirectories = totalDirectories,
                    knownUncompressedBytes = knownUncompressedBytes,
                    skippedUnsafeEntries = skippedUnsafeEntries,
                    previewEntries = previewEntries
                )
            }
        }
    }

    suspend fun extractZip(
        archive: PendingArchiveFile,
        inspection: ArchiveInspectionResult?,
        options: ArchiveExtractionOptions,
        onProgress: (ArchiveExtractionProgress) -> Unit
    ): ArchiveExtractionResult = withContext(Dispatchers.IO) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return@withContext extractZipToDownloads(archive, inspection, options, onProgress)
        }

        extractZipToAppFiles(archive, inspection, options, onProgress)
    }

    private suspend fun extractZipToAppFiles(
        archive: PendingArchiveFile,
        inspection: ArchiveInspectionResult?,
        options: ArchiveExtractionOptions,
        onProgress: (ArchiveExtractionProgress) -> Unit
    ): ArchiveExtractionResult {
        val archiveFile = requireZipFile(archive)
        val outputDirectory = createOutputDirectory(archive.fileName)
        var processedEntries = 0
        var extractedFiles = 0
        var createdDirectories = 0
        var writtenBytes = 0L
        var skippedUnsafeEntries = 0
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)

        archiveFile.inputStream().use { input ->
            ZipInputStream(input).use { zip ->
                while (true) {
                    coroutineContext.ensureActive()
                    val entry = zip.nextEntry ?: break
                    processedEntries += 1
                    val safeName = safeRelativePath(entry.name)
                    if (safeName == null) {
                        skippedUnsafeEntries += 1
                        zip.closeEntry()
                        continue
                    }

                    onProgress(
                        ArchiveExtractionProgress(
                            processedEntries = processedEntries,
                            totalEntries = inspection?.totalEntries ?: 0,
                            extractedFiles = extractedFiles,
                            writtenBytes = writtenBytes,
                            currentEntry = safeName
                        )
                    )

                    val target = resolveTargetFile(outputDirectory, safeName, entry.isDirectory, options)
                    if (entry.isDirectory) {
                        if (!target.exists() && target.mkdirs()) {
                            createdDirectories += 1
                        }
                        zip.closeEntry()
                        continue
                    }

                    target.parentFile?.let { parent ->
                        if (!parent.exists() && parent.mkdirs()) {
                            createdDirectories += 1
                        }
                    }

                    val writableTarget = if (options.overwriteExisting) target else uniqueFile(target)
                    writableTarget.outputStream().use { output ->
                        while (true) {
                            coroutineContext.ensureActive()
                            val read = zip.read(buffer)
                            if (read < 0) {
                                break
                            }
                            output.write(buffer, 0, read)
                            writtenBytes += read
                        }
                    }
                    extractedFiles += 1
                    zip.closeEntry()
                }
            }
        }

        return ArchiveExtractionResult(
            outputDirectoryPath = outputDirectory.absolutePath,
            processedEntries = processedEntries,
            extractedFiles = extractedFiles,
            createdDirectories = createdDirectories,
            writtenBytes = writtenBytes,
            skippedUnsafeEntries = skippedUnsafeEntries
        )
    }

    @androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
    private suspend fun extractZipToDownloads(
        archive: PendingArchiveFile,
        inspection: ArchiveInspectionResult?,
        options: ArchiveExtractionOptions,
        onProgress: (ArchiveExtractionProgress) -> Unit
    ): ArchiveExtractionResult {
        val archiveFile = requireZipFile(archive)
        val outputRoot = createDownloadsRelativeRoot(archive.fileName)
        val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        val resolver = context.contentResolver
        var processedEntries = 0
        var extractedFiles = 0
        var createdDirectories = 0
        var writtenBytes = 0L
        var skippedUnsafeEntries = 0
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)

        archiveFile.inputStream().use { input ->
            ZipInputStream(input).use { zip ->
                while (true) {
                    coroutineContext.ensureActive()
                    val entry = zip.nextEntry ?: break
                    processedEntries += 1
                    val safeName = safeRelativePath(entry.name)
                    if (safeName == null) {
                        skippedUnsafeEntries += 1
                        zip.closeEntry()
                        continue
                    }

                    onProgress(
                        ArchiveExtractionProgress(
                            processedEntries = processedEntries,
                            totalEntries = inspection?.totalEntries ?: 0,
                            extractedFiles = extractedFiles,
                            writtenBytes = writtenBytes,
                            currentEntry = safeName
                        )
                    )

                    if (entry.isDirectory) {
                        createdDirectories += 1
                        zip.closeEntry()
                        continue
                    }

                    val relativeFilePath = if (options.keepFolders) {
                        safeName
                    } else {
                        safeName.substringAfterLast('/')
                    }
                    val displayName = relativeFilePath.substringAfterLast('/').ifBlank {
                        "file_${processedEntries}"
                    }
                    val parentPath = relativeFilePath.substringBeforeLast('/', missingDelimiterValue = "")
                    val relativePath = buildDownloadsRelativePath(outputRoot, parentPath)
                    if (options.overwriteExisting) {
                        deleteExistingDownload(collection, relativePath, displayName)
                    }
                    val finalDisplayName = if (options.overwriteExisting) {
                        displayName
                    } else {
                        uniqueDownloadDisplayName(collection, relativePath, displayName)
                    }

                    val values = ContentValues().apply {
                        put(MediaStore.Downloads.DISPLAY_NAME, finalDisplayName)
                        put(MediaStore.Downloads.MIME_TYPE, guessMimeType(finalDisplayName))
                        put(MediaStore.Downloads.RELATIVE_PATH, relativePath)
                        put(MediaStore.Downloads.IS_PENDING, 1)
                    }
                    val uri = resolver.insert(collection, values)
                        ?: throw IOException("Android не дал создать файл в Downloads.")

                    try {
                        resolver.openOutputStream(uri)?.use { output ->
                            while (true) {
                                coroutineContext.ensureActive()
                                val read = zip.read(buffer)
                                if (read < 0) {
                                    break
                                }
                                output.write(buffer, 0, read)
                                writtenBytes += read
                            }
                        } ?: throw IOException("Android не дал записать файл в Downloads.")

                        val publishValues = ContentValues().apply {
                            put(MediaStore.Downloads.IS_PENDING, 0)
                        }
                        resolver.update(uri, publishValues, null, null)
                    } catch (error: Exception) {
                        resolver.delete(uri, null, null)
                        throw error
                    }

                    extractedFiles += 1
                    zip.closeEntry()
                }
            }
        }

        return ArchiveExtractionResult(
            outputDirectoryPath = outputRoot,
            processedEntries = processedEntries,
            extractedFiles = extractedFiles,
            createdDirectories = createdDirectories,
            writtenBytes = writtenBytes,
            skippedUnsafeEntries = skippedUnsafeEntries
        )
    }

    private fun requireZipFile(archive: PendingArchiveFile): File {
        if (!isSupportedZip(archive.fileName, archive.contentType)) {
            throw IOException("Поддерживается только ZIP-архив.")
        }
        val file = File(archive.localPath)
        if (!file.exists() || !file.isFile) {
            throw IOException("Файл архива не найден.")
        }
        return file
    }

    private fun createOutputDirectory(fileName: String): File {
        val baseDirectory = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
            ?: File(context.filesDir, "downloads")
        val archiveName = sanitizeFileName(fileName.substringBeforeLast('.', fileName)).ifBlank { "archive" }
        val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
        return File(baseDirectory, "Ikar/Archives/${archiveName}_$timestamp").apply { mkdirs() }
    }

    private fun createDownloadsRelativeRoot(fileName: String): String {
        val archiveName = sanitizeFileName(fileName.substringBeforeLast('.', fileName)).ifBlank { "archive" }
        val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
        return "${Environment.DIRECTORY_DOWNLOADS}/Ikar/Archives/${archiveName}_$timestamp"
    }

    private fun buildDownloadsRelativePath(outputRoot: String, parentPath: String): String {
        val normalizedParent = parentPath.trim('/').takeIf { it.isNotBlank() }
        val path = if (normalizedParent == null) outputRoot else "$outputRoot/$normalizedParent"
        return path.trim('/') + "/"
    }

    private fun resolveTargetFile(
        outputDirectory: File,
        relativePath: String,
        isDirectory: Boolean,
        options: ArchiveExtractionOptions
    ): File {
        val path = if (options.keepFolders) {
            relativePath
        } else {
            relativePath.trimEnd('/').substringAfterLast('/')
        }
        val target = File(outputDirectory, path)
        val canonicalOutput = outputDirectory.canonicalFile
        val canonicalTarget = if (isDirectory) {
            target.canonicalFile
        } else {
            target.parentFile?.canonicalFile ?: outputDirectory.canonicalFile
        }
        if (canonicalTarget != canonicalOutput && !canonicalTarget.path.startsWith(canonicalOutput.path + File.separator)) {
            throw IOException("Архив пытается записать файл вне папки распаковки.")
        }
        return target
    }

    private fun safeRelativePath(name: String): String? {
        val normalized = name.replace('\\', '/').trim().trimStart('/')
        if (normalized.isBlank()) {
            return null
        }
        val segments = normalized.split('/').filter { it.isNotBlank() }
        if (segments.any { it == "." || it == ".." || it.contains(':') }) {
            return null
        }
        return segments.joinToString("/")
    }

    private fun uniqueFile(target: File): File {
        if (!target.exists()) {
            return target
        }
        val parent = target.parentFile ?: return target
        val baseName = target.name.substringBeforeLast('.', target.name)
        val extension = target.name.substringAfterLast('.', missingDelimiterValue = "")
            .takeIf { it != target.name }
            ?.let { ".$it" }
            .orEmpty()
        var index = 1
        while (true) {
            val candidate = File(parent, "${baseName}_$index$extension")
            if (!candidate.exists()) {
                return candidate
            }
            index += 1
        }
    }

    @androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
    private fun uniqueDownloadDisplayName(collection: Uri, relativePath: String, displayName: String): String {
        if (!downloadExists(collection, relativePath, displayName)) {
            return displayName
        }

        val baseName = displayName.substringBeforeLast('.', displayName)
        val extension = displayName.substringAfterLast('.', missingDelimiterValue = "")
            .takeIf { it != displayName }
            ?.let { ".$it" }
            .orEmpty()
        var index = 1
        while (true) {
            val candidate = "${baseName}_$index$extension"
            if (!downloadExists(collection, relativePath, candidate)) {
                return candidate
            }
            index += 1
        }
    }

    @androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
    private fun downloadExists(collection: Uri, relativePath: String, displayName: String): Boolean =
        queryDownloadIds(collection, relativePath, displayName).isNotEmpty()

    @androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
    private fun deleteExistingDownload(collection: Uri, relativePath: String, displayName: String) {
        queryDownloadIds(collection, relativePath, displayName).forEach { id ->
            context.contentResolver.delete(ContentUris.withAppendedId(collection, id), null, null)
        }
    }

    @androidx.annotation.RequiresApi(Build.VERSION_CODES.Q)
    private fun queryDownloadIds(collection: Uri, relativePath: String, displayName: String): List<Long> {
        val ids = mutableListOf<Long>()
        context.contentResolver.query(
            collection,
            arrayOf(MediaStore.MediaColumns._ID),
            "${MediaStore.Downloads.DISPLAY_NAME} = ? AND ${MediaStore.Downloads.RELATIVE_PATH} = ?",
            arrayOf(displayName, relativePath),
            null
        )?.use { cursor ->
            val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
            while (idIndex >= 0 && cursor.moveToNext()) {
                ids += cursor.getLong(idIndex)
            }
        }
        return ids
    }

    private fun queryDisplayName(uri: Uri): String? {
        val resolver = context.contentResolver
        resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
            val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
            if (nameIndex >= 0 && cursor.moveToFirst() && !cursor.isNull(nameIndex)) {
                return cursor.getString(nameIndex)
            }
        }
        return null
    }

    private fun queryFileSize(uri: Uri): Long? {
        val resolver = context.contentResolver
        resolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { cursor ->
            val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
            if (sizeIndex >= 0 && cursor.moveToFirst() && !cursor.isNull(sizeIndex)) {
                return cursor.getLong(sizeIndex).takeIf { it >= 0L }
            }
        }
        return null
    }

    private fun copyToFileWithLimit(input: InputStream, target: File): Long {
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        var totalBytes = 0L
        target.outputStream().use { output ->
            while (true) {
                val read = input.read(buffer)
                if (read < 0) {
                    return totalBytes
                }
                totalBytes += read
                if (totalBytes > maxArchiveImportBytes) {
                    throw IOException("Архив слишком большой: максимум 512 МБ.")
                }
                output.write(buffer, 0, read)
            }
        }
    }

    private fun sanitizeFileName(value: String): String {
        val normalized = Normalizer.normalize(value.trim(), Normalizer.Form.NFKD)
        return normalized
            .replace(Regex("[^A-Za-z0-9._-]+"), "_")
            .trim('_', '.', '-')
            .ifBlank { "archive" }
    }

    private fun guessMimeType(fileName: String): String {
        val extension = fileName.substringAfterLast('.', missingDelimiterValue = "")
            .takeIf { it.isNotBlank() }
            ?.lowercase()
        return extension
            ?.let(MimeTypeMap.getSingleton()::getMimeTypeFromExtension)
            ?: "application/octet-stream"
    }

    companion object {
        private const val MaxPreviewEntries = 30

        fun isSupportedZip(fileName: String?, contentType: String?): Boolean {
            val normalizedName = fileName.orEmpty().lowercase()
            val normalizedType = contentType.orEmpty().lowercase()
            return normalizedName.endsWith(".zip") ||
                normalizedType == "application/zip" ||
                normalizedType == "application/x-zip" ||
                normalizedType == "application/x-zip-compressed"
        }
    }
}
