import Compression
import Foundation
import zlib

struct ZipUtils {

    private static let logTag = "ZipUtils"

    private static let DATA_JSON_FILENAME = "data.json"
    private static let IMAGES_DIR_NAME = "images"
    private static let METADATA_FILENAME = "metadata.txt"

    static func createExportZip(
        exportData: ClimbDataBackup,
        referencedImagePaths: Set<String>
    ) throws -> Data {

        var zipData = Data()
        var centralDirectory = Data()
        var fileEntries: [(name: String, data: Data, offset: UInt32)] = []
        var currentOffset: UInt32 = 0

        // Add metadata
        let metadata = createMetadata(
            exportData: exportData, referencedImagePaths: referencedImagePaths)
        let metadataData = metadata.data(using: .utf8) ?? Data()
        try addFileToZip(
            filename: METADATA_FILENAME,
            fileData: metadataData,
            zipData: &zipData,
            fileEntries: &fileEntries,
            currentOffset: &currentOffset
        )

        // Encode JSON data
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        encoder.dateEncodingStrategy = .custom { date, encoder in
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
            var container = encoder.singleValueContainer()
            try container.encode(formatter.string(from: date))
        }
        let jsonData = try encoder.encode(exportData)
        try addFileToZip(
            filename: DATA_JSON_FILENAME,
            fileData: jsonData,
            zipData: &zipData,
            fileEntries: &fileEntries,
            currentOffset: &currentOffset
        )

        // Process images in batches for better performance
        logInfo("Processing \(referencedImagePaths.count) images for export")
        var successfulImages = 0
        let batchSize = 10
        let sortedPaths = Array(referencedImagePaths).sorted()

        // Pre-allocate capacity for better memory performance
        zipData.reserveCapacity(zipData.count + (referencedImagePaths.count * 200_000))  // Estimate 200KB per image

        for (index, imagePath) in sortedPaths.enumerated() {
            if index % batchSize == 0 {
                logInfo("Processing images \(index)/\(sortedPaths.count)")
            }

            let imageURL = URL(fileURLWithPath: imagePath)
            let imageName = imageURL.lastPathComponent

            guard FileManager.default.fileExists(atPath: imagePath) else {
                continue
            }

            do {
                let imageData = try Data(contentsOf: imageURL)
                if !imageData.isEmpty {
                    let imageEntryName = "\(IMAGES_DIR_NAME)/\(imageName)"
                    try addFileToZip(
                        filename: imageEntryName,
                        fileData: imageData,
                        zipData: &zipData,
                        fileEntries: &fileEntries,
                        currentOffset: &currentOffset
                    )
                    successfulImages += 1
                }
            } catch {
                logWarning("Failed to read image: \(imageName)")
            }
        }

        logInfo("Export: included \(successfulImages)/\(referencedImagePaths.count) images")

        // Build central directory
        centralDirectory.reserveCapacity(fileEntries.count * 100)  // Estimate 100 bytes per entry
        for entry in fileEntries {
            let centralDirEntry = createCentralDirectoryEntry(
                filename: entry.name,
                fileData: entry.data,
                localHeaderOffset: entry.offset
            )
            centralDirectory.append(centralDirEntry)
        }

        let centralDirOffset = UInt32(zipData.count)
        zipData.append(centralDirectory)

        let endOfCentralDir = createEndOfCentralDirectory(
            numEntries: UInt16(fileEntries.count),
            centralDirSize: UInt32(centralDirectory.count),
            centralDirOffset: centralDirOffset
        )
        zipData.append(endOfCentralDir)

        return zipData
    }

    static func extractImportZip(data: Data) throws -> ImportResult {
        logInfo("Starting ZIP extraction - data size: \(data.count) bytes")

        return try extractUsingCustomParser(data: data)
    }

    private static func extractUsingCustomParser(data: Data) throws -> ImportResult {
        var jsonContent = ""
        var metadataContent = ""
        var importedImagePaths: [String: String] = [:]

        let zipEntries: [ZipEntry]
        do {
            zipEntries = try parseZipFile(data: data)
            logInfo("Successfully parsed ZIP file with \(zipEntries.count) entries")
        } catch {
            logError("Failed to parse ZIP file: \(error)")
            logError(
                "ZIP data header: \(data.prefix(20).map { String(format: "%02X", $0) }.joined(separator: " "))"
            )
            throw NSError(
                domain: "ImportError", code: 1,
                userInfo: [
                    NSLocalizedDescriptionKey:
                        "Failed to parse ZIP file: \(error.localizedDescription). This may be due to incompatibility with the ZIP format."
                ]
            )
        }

        logInfo("Found \(zipEntries.count) entries in ZIP file:")
        for entry in zipEntries {
            logInfo("  - \(entry.filename) (size: \(entry.data.count) bytes)")
        }

        for entry in zipEntries {
            switch entry.filename {
            case METADATA_FILENAME:
                metadataContent = String(data: entry.data, encoding: .utf8) ?? ""
                logInfo("Found metadata: \(metadataContent.prefix(100))...")

            case DATA_JSON_FILENAME:
                jsonContent = String(data: entry.data, encoding: .utf8) ?? ""
                logInfo("Found data.json with \(jsonContent.count) characters")
                if jsonContent.isEmpty {
                    logWarning("WARNING: data.json is empty!")
                } else {
                    logInfo("data.json preview: \(jsonContent.prefix(200))...")
                }

            default:
                if entry.filename.hasPrefix("\(IMAGES_DIR_NAME)/") && !entry.filename.hasSuffix("/")
                {
                    let originalFilename = String(
                        entry.filename.dropFirst("\(IMAGES_DIR_NAME)/".count))

                    do {
                        let filename = try ImageManager.shared.saveImportedImage(
                            entry.data, filename: originalFilename)
                        importedImagePaths[originalFilename] = filename
                    } catch {
                        logError("Failed to import image \(originalFilename): \(error)")
                    }
                }
            }
        }

        guard !jsonContent.isEmpty else {
            logError("ERROR: data.json not found or empty")
            logInfo("Available files in ZIP:")
            for entry in zipEntries {
                logInfo("  - \(entry.filename)")
            }
            throw NSError(
                domain: "ImportError", code: 1,
                userInfo: [
                    NSLocalizedDescriptionKey:
                        "Invalid ZIP file: data.json not found or empty. Found files: \(zipEntries.map { $0.filename }.joined(separator: ", "))"
                ]
            )
        }

        logInfo("Import extraction completed: \(importedImagePaths.count) images processed")

        return ImportResult(
            jsonData: jsonContent.data(using: .utf8) ?? Data(), imagePathMapping: importedImagePaths
        )
    }

    private static func logInfo(_ message: String) {
        AppLogger.info(message, tag: logTag)
    }

    private static func logWarning(_ message: String) {
        AppLogger.warning(message, tag: logTag)
    }

    private static func logError(_ message: String) {
        AppLogger.error(message, tag: logTag)
    }

    private static func createMetadata(
        exportData: ClimbDataBackup,
        referencedImagePaths: Set<String>
    ) -> String {
        return """
            Ascently Export Metadata
            ========================
            Export Date: \(exportData.exportedAt)
            Gyms: \(exportData.gyms.count)
            Problems: \(exportData.problems.count)
            Sessions: \(exportData.sessions.count)
            Attempts: \(exportData.attempts.count)
            Referenced Images: \(referencedImagePaths.count)
            Format: ZIP with embedded JSON data and images
            """
    }

    private static func addFileToZip(
        filename: String,
        fileData: Data,
        zipData: inout Data,
        fileEntries: inout [(name: String, data: Data, offset: UInt32)],
        currentOffset: inout UInt32
    ) throws {

        let localHeader = createLocalFileHeader(filename: filename, fileData: fileData)
        let headerOffset = currentOffset

        zipData.append(localHeader)
        zipData.append(fileData)

        fileEntries.append((name: filename, data: fileData, offset: headerOffset))

        currentOffset += UInt32(localHeader.count + fileData.count)
    }

    private static func createLocalFileHeader(filename: String, fileData: Data) -> Data {
        var header = Data()

        header.append(Data([0x50, 0x4b, 0x03, 0x04]))

        header.append(Data([0x14, 0x00]))

        header.append(Data([0x00, 0x00]))

        header.append(Data([0x00, 0x00]))

        // Last mod file time & date (use current time)
        let dosTime = getDosDateTime()
        header.append(dosTime)

        let crc = calculateCRC32(data: fileData)
        header.append(withUnsafeBytes(of: crc.littleEndian) { Data($0) })

        // Compressed size (same as uncompressed since no compression)
        let compressedSize = UInt32(fileData.count)
        header.append(withUnsafeBytes(of: compressedSize.littleEndian) { Data($0) })

        let uncompressedSize = UInt32(fileData.count)
        header.append(withUnsafeBytes(of: uncompressedSize.littleEndian) { Data($0) })

        let filenameData = filename.data(using: .utf8) ?? Data()
        let filenameLength = UInt16(filenameData.count)
        header.append(withUnsafeBytes(of: filenameLength.littleEndian) { Data($0) })

        header.append(Data([0x00, 0x00]))

        header.append(filenameData)

        return header
    }

    private static func createCentralDirectoryEntry(
        filename: String,
        fileData: Data,
        localHeaderOffset: UInt32
    ) -> Data {
        var entry = Data()

        entry.append(Data([0x50, 0x4b, 0x01, 0x02]))

        entry.append(Data([0x14, 0x00]))

        entry.append(Data([0x14, 0x00]))

        entry.append(Data([0x00, 0x00]))

        entry.append(Data([0x00, 0x00]))

        // Last mod file time & date
        let dosTime = getDosDateTime()
        entry.append(dosTime)

        let crc = calculateCRC32(data: fileData)
        entry.append(withUnsafeBytes(of: crc.littleEndian) { Data($0) })

        let compressedSize = UInt32(fileData.count)
        entry.append(withUnsafeBytes(of: compressedSize.littleEndian) { Data($0) })

        let uncompressedSize = UInt32(fileData.count)
        entry.append(withUnsafeBytes(of: uncompressedSize.littleEndian) { Data($0) })

        let filenameData = filename.data(using: .utf8) ?? Data()
        let filenameLength = UInt16(filenameData.count)
        entry.append(withUnsafeBytes(of: filenameLength.littleEndian) { Data($0) })

        entry.append(Data([0x00, 0x00]))

        // File comment length
        entry.append(Data([0x00, 0x00]))

        entry.append(Data([0x00, 0x00]))

        entry.append(Data([0x00, 0x00]))

        entry.append(Data([0x00, 0x00, 0x00, 0x00]))

        // Relative offset of local header
        entry.append(withUnsafeBytes(of: localHeaderOffset.littleEndian) { Data($0) })

        entry.append(filenameData)

        return entry
    }

    private static func createEndOfCentralDirectory(
        numEntries: UInt16,
        centralDirSize: UInt32,
        centralDirOffset: UInt32
    ) -> Data {
        var endRecord = Data()

        endRecord.append(Data([0x50, 0x4b, 0x05, 0x06]))

        endRecord.append(Data([0x00, 0x00]))

        // Number of the disk with the start of the central directory
        endRecord.append(Data([0x00, 0x00]))

        // Total number of entries in the central directory on this disk
        endRecord.append(withUnsafeBytes(of: numEntries.littleEndian) { Data($0) })

        // Total number of entries in the central directory
        endRecord.append(withUnsafeBytes(of: numEntries.littleEndian) { Data($0) })

        endRecord.append(withUnsafeBytes(of: centralDirSize.littleEndian) { Data($0) })

        // Offset of start of central directory
        endRecord.append(withUnsafeBytes(of: centralDirOffset.littleEndian) { Data($0) })

        // ZIP file comment length
        endRecord.append(Data([0x00, 0x00]))

        return endRecord
    }

    private static func getDosDateTime() -> Data {
        let date = Date()
        let calendar = Calendar.current
        let components = calendar.dateComponents(
            [.year, .month, .day, .hour, .minute, .second], from: date)

        let year = UInt16(max(1980, components.year ?? 1980) - 1980)
        let month = UInt16(components.month ?? 1)
        let day = UInt16(components.day ?? 1)
        let hour = UInt16(components.hour ?? 0)
        let minute = UInt16(components.minute ?? 0)
        let second = UInt16((components.second ?? 0) / 2)

        let dosDate = (year << 9) | (month << 5) | day
        let dosTime = (hour << 11) | (minute << 5) | second

        var data = Data()
        data.append(withUnsafeBytes(of: dosTime.littleEndian) { Data($0) })
        data.append(withUnsafeBytes(of: dosDate.littleEndian) { Data($0) })
        return data
    }

    // CRC32 lookup table for faster calculation
    private static let crc32Table: [UInt32] = {
        let polynomial: UInt32 = 0xEDB8_8320
        var table = [UInt32](repeating: 0, count: 256)
        for i in 0..<256 {
            var crc = UInt32(i)
            for _ in 0..<8 {
                if crc & 1 != 0 {
                    crc = (crc >> 1) ^ polynomial
                } else {
                    crc >>= 1
                }
            }
            table[i] = crc
        }
        return table
    }()

    private static func calculateCRC32(data: Data) -> UInt32 {
        var crc: UInt32 = 0xFFFF_FFFF

        data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
            for byte in bytes {
                let index = Int((crc ^ UInt32(byte)) & 0xFF)
                crc = (crc >> 8) ^ crc32Table[index]
            }
        }

        return ~crc
    }

    private static func parseZipFile(data: Data) throws -> [ZipEntry] {

        var endOfCentralDirOffset = -1
        let signature = Data([0x50, 0x4b, 0x05, 0x06])

        for i in stride(from: data.count - 22, through: 0, by: -1) {
            if data.subdata(in: i..<i + 4) == signature {
                endOfCentralDirOffset = i
                break
            }
        }

        guard endOfCentralDirOffset >= 0 else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "End of central directory not found"])
        }

        let endRecord = data.subdata(in: endOfCentralDirOffset..<endOfCentralDirOffset + 22)
        let numEntries = endRecord.subdata(in: 8..<10).withUnsafeBytes { $0.load(as: UInt16.self) }
        let centralDirOffset = endRecord.subdata(in: 16..<20).withUnsafeBytes {
            $0.load(as: UInt32.self)
        }

        var entries: [ZipEntry] = []
        var offset = Int(centralDirOffset)

        for _ in 0..<numEntries {
            let entry = try parseCentralDirectoryEntry(data: data, offset: &offset)
            entries.append(entry)
        }

        var zipEntries: [ZipEntry] = []
        for entry in entries {
            let fileData = try extractFileData(data: data, entry: entry)
            let zipEntry = ZipEntry(filename: entry.filename, data: fileData)
            zipEntries.append(zipEntry)
        }

        return zipEntries
    }

    private static func parseCentralDirectoryEntry(
        data: Data, offset: inout Int
    ) throws -> ZipEntry {
        guard offset + 46 <= data.count else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Invalid central directory entry"])
        }

        let entryData = data.subdata(in: offset..<offset + 46)

        let signature = entryData.subdata(in: 0..<4)
        guard signature == Data([0x50, 0x4b, 0x01, 0x02]) else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Invalid central directory signature"])
        }

        let compressionMethod = entryData.subdata(in: 10..<12).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }
        let compressedSize = entryData.subdata(in: 20..<24).withUnsafeBytes {
            $0.load(as: UInt32.self)
        }
        let uncompressedSize = entryData.subdata(in: 24..<28).withUnsafeBytes {
            $0.load(as: UInt32.self)
        }
        let filenameLength = entryData.subdata(in: 28..<30).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }
        let extraFieldLength = entryData.subdata(in: 30..<32).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }
        let commentLength = entryData.subdata(in: 32..<34).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }
        let localHeaderOffset = entryData.subdata(in: 42..<46).withUnsafeBytes {
            $0.load(as: UInt32.self)
        }

        offset += 46

        let filenameData = data.subdata(in: offset..<offset + Int(filenameLength))
        let filename = String(data: filenameData, encoding: .utf8) ?? ""
        offset += Int(filenameLength)

        offset += Int(extraFieldLength) + Int(commentLength)

        return ZipEntry(
            filename: filename,
            localHeaderOffset: localHeaderOffset,
            compressedSize: compressedSize,
            uncompressedSize: uncompressedSize,
            compressionMethod: compressionMethod
        )
    }

    private static func extractFileData(
        data: Data, entry: ZipEntry
    ) throws -> Data {
        let headerOffset = Int(entry.localHeaderOffset)
        guard headerOffset + 30 <= data.count else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Invalid local header offset"])
        }

        let headerData = data.subdata(in: headerOffset..<headerOffset + 30)

        let signature = headerData.subdata(in: 0..<4)
        guard signature == Data([0x50, 0x4b, 0x03, 0x04]) else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Invalid local header signature"])
        }

        let localCompressionMethod = headerData.subdata(in: 8..<10).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }

        let filenameLength = headerData.subdata(in: 26..<28).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }
        let extraFieldLength = headerData.subdata(in: 28..<30).withUnsafeBytes {
            $0.load(as: UInt16.self)
        }

        let dataOffset = headerOffset + 30 + Int(filenameLength) + Int(extraFieldLength)
        let dataEndOffset = dataOffset + Int(entry.compressedSize)

        guard dataEndOffset <= data.count else {
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "File data extends beyond ZIP file"])
        }

        let compressedData = data.subdata(in: dataOffset..<dataEndOffset)

        switch localCompressionMethod {
        case 0:
            return compressedData
        case 8:
            return try decompressDeflate(compressedData)
        default:
            throw NSError(
                domain: "ZipError", code: 1,
                userInfo: [
                    NSLocalizedDescriptionKey:
                        "Unsupported compression method: \(localCompressionMethod)"
                ])
        }
    }

    private static func decompressDeflate(_ data: Data) throws -> Data {
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 1024 * 1024)
        defer { buffer.deallocate() }

        var decompressedData = Data()

        try data.withUnsafeBytes { bytes in
            var stream = z_stream()
            stream.next_in = UnsafeMutablePointer<UInt8>(
                mutating: bytes.bindMemory(to: UInt8.self).baseAddress)
            stream.avail_in = UInt32(data.count)

            let initResult = inflateInit2_(
                &stream, -15, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size))
            guard initResult == Z_OK else {
                throw NSError(
                    domain: "ZipError", code: 1,
                    userInfo: [
                        NSLocalizedDescriptionKey: "Failed to initialize deflate decompression"
                    ])
            }

            defer { inflateEnd(&stream) }

            var result: Int32

            repeat {
                stream.next_out = buffer
                stream.avail_out = 1024 * 1024

                result = inflate(&stream, Z_NO_FLUSH)

                if result != Z_OK && result != Z_STREAM_END {
                    throw NSError(
                        domain: "ZipError", code: 1,
                        userInfo: [
                            NSLocalizedDescriptionKey: "Decompression failed with code: \(result)"
                        ])
                }

                let bytesDecompressed = 1024 * 1024 - Int(stream.avail_out)
                if bytesDecompressed > 0 {
                    decompressedData.append(buffer, count: bytesDecompressed)
                }
            } while result != Z_STREAM_END
        }

        return decompressedData
    }
}

struct ZipEntry {
    let filename: String
    let data: Data
    let localHeaderOffset: UInt32
    let compressedSize: UInt32
    let uncompressedSize: UInt32
    let compressionMethod: UInt16

    init(filename: String, data: Data) {
        self.filename = filename
        self.data = data
        self.localHeaderOffset = 0
        self.compressedSize = 0
        self.uncompressedSize = 0
        self.compressionMethod = 0
    }

    init(
        filename: String, localHeaderOffset: UInt32, compressedSize: UInt32,
        uncompressedSize: UInt32 = 0, compressionMethod: UInt16 = 0
    ) {
        self.filename = filename
        self.data = Data()
        self.localHeaderOffset = localHeaderOffset
        self.compressedSize = compressedSize
        self.uncompressedSize = uncompressedSize
        self.compressionMethod = compressionMethod
    }
}

struct ImportResult {
    let jsonData: Data
    let imagePathMapping: [String: String]
}
