//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationArchive
import Foundation
import SystemPackage

extension EXT4.EXT4Reader {
    public func export(archive: FilePath) throws {
        let config = ArchiveWriterConfiguration(
            format: .paxRestricted, filter: .none, options: [Options.xattrformat(.schily)])
        let writer = try ArchiveWriter(configuration: config)
        try writer.open(file: archive.url)
        var items = self.tree.root.pointee.children
        let hardlinkedInodes = Set(self.hardlinks.values)
        var hardlinkTargets: [EXT4.InodeNumber: FilePath] = [:]

        while items.count > 0 {
            let itemPtr = items.removeFirst()
            let item = itemPtr.pointee
            let inode = try self.getInode(number: item.inode)
            let entry = WriteEntry()
            let mode = inode.mode
            let size: UInt64 = (UInt64(inode.sizeHigh) << 32) | UInt64(inode.sizeLow)
            entry.permissions = mode_t(mode)
            guard let path = item.path else {
                continue
            }
            if hardlinkedInodes.contains(item.inode) {
                hardlinkTargets[item.inode] = path
            }
            guard self.hardlinks[path] == nil else {
                continue
            }
            var attributes: [EXT4.ExtendedAttribute] = []
            let buffer: [UInt8] = EXT4.tupleToArray(inode.inlineXattrs)
            if !buffer.allZeros {
                try attributes.append(contentsOf: Self.readInlineExtendedAttributes(from: buffer))
            }
            if inode.xattrBlockLow != 0 {
                let block = inode.xattrBlockLow
                try self.seek(block: block)
                guard let buffer = try self.handle.read(upToCount: Int(self.blockSize)) else {
                    throw EXT4.Error.couldNotReadBlock(block)
                }
                try attributes.append(contentsOf: Self.readBlockExtendedAttributes(from: [UInt8](buffer)))
            }

            var xattrs: [String: Data] = [:]
            for attribute in attributes {
                guard attribute.fullName != "system.data" else {
                    continue
                }
                xattrs[attribute.fullName] = Data(attribute.value)
            }

            let pathStr = path.description
            entry.path = pathStr
            entry.size = Int64(size)
            entry.group = gid_t(inode.gidHigh) << 16 | gid_t(inode.gid)
            entry.owner = uid_t(inode.uidHigh) << 16 | uid_t(inode.uid)
            entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
            entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
            entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
            entry.xattrs = xattrs

            if mode.isDir() {
                entry.fileType = .directory
                for child in item.children {
                    items.append(child)
                }
                if pathStr == "" {
                    continue
                }
                try writer.writeEntry(entry: entry, data: nil)
            } else if mode.isReg() {
                entry.fileType = .regular
                var data = Data()
                var remaining: UInt64 = size
                if let block = item.blocks {
                    for dataBlock in block.start..<block.end {
                        try self.seek(block: dataBlock)
                        var count: UInt64
                        if remaining > self.blockSize {
                            count = self.blockSize
                        } else {
                            count = remaining
                        }
                        guard let dataBytes = try self.handle.read(upToCount: Int(count)) else {
                            throw EXT4.Error.couldNotReadBlock(dataBlock)
                        }
                        data.append(dataBytes)
                        remaining -= UInt64(dataBytes.count)
                    }
                }
                if let additionalBlocks = item.additionalBlocks {
                    for block in additionalBlocks {
                        for dataBlock in block.start..<block.end {
                            try self.seek(block: dataBlock)
                            var count: UInt64
                            if remaining > self.blockSize {
                                count = self.blockSize
                            } else {
                                count = remaining
                            }
                            guard let dataBytes = try self.handle.read(upToCount: Int(count)) else {
                                throw EXT4.Error.couldNotReadBlock(dataBlock)
                            }
                            data.append(dataBytes)
                            remaining -= UInt64(dataBytes.count)
                        }
                    }
                }
                try writer.writeEntry(entry: entry, data: data)
            } else if mode.isLink() {
                entry.fileType = .symbolicLink
                if size < 60 {
                    let linkBytes = EXT4.tupleToArray(inode.block)
                    entry.symlinkTarget = String(bytes: linkBytes.prefix(Int(size)), encoding: .utf8) ?? ""
                } else {
                    if let block = item.blocks {
                        try self.seek(block: block.start)
                        guard let linkBytes = try self.handle.read(upToCount: Int(size)) else {
                            throw EXT4.Error.couldNotReadBlock(block.start)
                        }
                        entry.symlinkTarget = String(bytes: linkBytes, encoding: .utf8) ?? ""
                    }
                }
                try writer.writeEntry(entry: entry, data: nil)
            } else {  // do not process sockets, fifo, character and block devices
                continue
            }
        }
        for (path, number) in self.hardlinks {
            guard let targetPath = hardlinkTargets[number] else {
                continue
            }
            let inode = try self.getInode(number: number)
            let entry = WriteEntry()
            entry.path = path.description
            entry.hardlink = targetPath.description
            entry.permissions = mode_t(inode.mode)
            entry.group = gid_t(inode.gidHigh) << 16 | gid_t(inode.gid)
            entry.owner = uid_t(inode.uidHigh) << 16 | uid_t(inode.uid)
            entry.creationDate = Date(fsTimestamp: UInt64(inode.crtimeExtra) << 32 | UInt64(inode.crtime))
            entry.modificationDate = Date(fsTimestamp: UInt64(inode.mtimeExtra) << 32 | UInt64(inode.mtime))
            entry.contentAccessDate = Date(fsTimestamp: UInt64(inode.atimeExtra) << 32 | UInt64(inode.atime))
            try writer.writeEntry(entry: entry, data: nil)
        }
        try writer.finishEncoding()
    }

    @available(*, deprecated, renamed: "readInlineExtendedAttributes(from:)")
    public static func readInlineExtenedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
        try readInlineExtendedAttributes(from: buffer)
    }

    public static func readInlineExtendedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
        let header = buffer[0..<4].withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
        if header != EXT4.XAttrHeaderMagic {
            throw EXT4.FileXattrsState.Error.missingXAttrHeader
        }
        return try EXT4.FileXattrsState.read(buffer: buffer, start: 4, offset: 4)
    }

    @available(*, deprecated, renamed: "readBlockExtendedAttributes(from:)")
    public static func readBlockExtenedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
        try readBlockExtendedAttributes(from: buffer)
    }

    public static func readBlockExtendedAttributes(from buffer: [UInt8]) throws -> [EXT4.ExtendedAttribute] {
        let header = buffer[0..<4].withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
        if header != EXT4.XAttrHeaderMagic {
            throw EXT4.FileXattrsState.Error.missingXAttrHeader
        }

        return try EXT4.FileXattrsState.read(buffer: [UInt8](buffer), start: 32, offset: 0)
    }

    func seek(block: UInt32) throws {
        try self.handle.seek(toOffset: UInt64(block) * blockSize)
    }
}

extension Date {
    init(fsTimestamp: UInt64) {
        if fsTimestamp == 0 {
            self = Date.distantPast
            return
        }

        // 32 bits - base: seconds since January 1, 1970, signed (negative for pre-1970 dates)
        // 2 bits - epoch: overflow counter (0-3), how many times the 32-bit seconds field has wrapped
        // 30 bits - nanoseconds (0-999,999,999)
        let base = Int32(truncatingIfNeeded: fsTimestamp)
        let epoch = Int64(fsTimestamp & 0x3_0000_0000)
        let seconds = Int64(base) + epoch
        let nanoseconds = Double(fsTimestamp >> 34) / 1_000_000_000

        self = Date(timeIntervalSince1970: Double(seconds) + nanoseconds)
    }
}
