package com.github.nullptroma.wallenc.domain.vault.storages.local

import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.storages.common.StorageSyncJournalBuffer
import com.github.nullptroma.wallenc.domain.vault.storages.common.StorageSyncJournalCodec
import com.github.nullptroma.wallenc.domain.vault.storages.common.readSystemFileBytesOrEmpty

import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournal
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalMerge
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncLock
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncRevision
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.Clock
import java.time.Instant
import java.util.UUID
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.fileSize
import kotlin.io.path.pathString
import kotlin.io.path.relativeTo

class LocalStorageAccessor(
    filesystemBasePath: String,
    private val ioDispatcher: CoroutineDispatcher
) : IStorageAccessor {
    private val syncActorId = UUID.randomUUID().toString()
    private val _filesystemBasePath: Path = Path(filesystemBasePath).normalize().absolute()

    private val _size = MutableStateFlow<Long?>(null)
    override val size: StateFlow<Long?> = _size

    private val _numberOfFiles = MutableStateFlow<Int?>(null)
    override val numberOfFiles: StateFlow<Int?> = _numberOfFiles

    private val _isAvailable = MutableStateFlow(false)
    override val isAvailable: StateFlow<Boolean> = _isAvailable

    private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>()
    override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates

    private val _dirsUpdates = MutableSharedFlow<DataPage<IDirectory>>()
    override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = _dirsUpdates

    private val journalBuffer = StorageSyncJournalBuffer(
        syncActorId = syncActorId,
        originStorageUuid = null,
        readJournal = { readSyncJournalUnchecked() },
        writeJournal = { writeSyncJournal(it) },
    )

    suspend fun init() = withContext(ioDispatcher) {
        // запускам сканирование хранилища
        scanSizeAndNumOfFiles()
    }

    /**
     * Проверяет существование корневого пути Storage в файловой системе, изменяет _isAvailable
     */
    private fun checkAvailable(): Boolean {
        _isAvailable.value = _filesystemBasePath.toFile().exists()
        return _isAvailable.value
    }

    /**
     * Перебирает все файлы в файловой системе
     * @param dir стартовый каталог
     * @param maxDepth максимальная глубина (отрицательное для бесконечной)
     * @param callback метод обратного вызова для каждого файла и директории
     */
    private suspend fun scanFileSystem(
        dir: File,
        maxDepth: Int,
        callback: suspend (File) -> Unit,
        useCallbackForSelf: Boolean = true
    ) {
        if (!dir.exists())
            return


        val children = dir.listFiles()
        if (children != null) {

            // вызвать коллбек для каждого элемента директории
            for (child in children) {
                if(child.name != SYSTEM_HIDDEN_DIRNAME)
                    callback(child)
            }

            if (useCallbackForSelf)
                callback(dir)

            if (maxDepth != 0) {
                val nextMaxDepth = if (maxDepth > 0) maxDepth - 1 else maxDepth
                for (child in children) {
                    if (child.isDirectory && child.name != SYSTEM_HIDDEN_DIRNAME) {
                        scanFileSystem(child, nextMaxDepth, callback, false)
                    }
                }
            }
        } else if (useCallbackForSelf) {
            callback(dir)
        }
    }

    private class LocalStorageFilePair private constructor(
        val file: File,
        val metaFile: File,
        val meta: CommonMetaInfo
    ) {

        companion object {
            private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }

            fun fromFile(filesystemBasePath: Path, file: File): LocalStorageFilePair? {
                if (!file.exists())
                    return null
                if (file.name.endsWith(META_INFO_POSTFIX))
                    return fromMetaFile(filesystemBasePath, file)

                val filePath = file.toPath()
                val metaFilePath = Path(
                    if (file.isFile) {
                        file.absolutePath + META_INFO_POSTFIX
                    } else {
                        Path(file.absolutePath, META_INFO_POSTFIX).pathString
                    }
                )
                val metaFile = metaFilePath.toFile()
                val metaInfo: CommonMetaInfo
                val storageFilePath = "/" + filePath.relativeTo(filesystemBasePath)

                if (!metaFile.exists()) {
                    metaInfo = CommonMetaInfo(
                        size = filePath.fileSize(),
                        path = storageFilePath
                    )
                    jackson.writeValue(metaFile, metaInfo)
                } else {
                    var readMeta: CommonMetaInfo
                    try {
                        val reader = metaFile.bufferedReader()
                        readMeta = jackson.readValue(reader)
                    } catch (e: JacksonException) {
                        // если файл повреждён - пересоздать
                        readMeta = CommonMetaInfo(
                            size = filePath.fileSize(),
                            path = storageFilePath
                        )
                        jackson.writeValue(metaFile, readMeta)
                    }
                    metaInfo = readMeta
                }
                return LocalStorageFilePair(
                    file = file,
                    metaFile = metaFile,
                    meta = metaInfo
                )
            }

            fun fromMetaFile(filesystemBasePath: Path, metaFile: File): LocalStorageFilePair? {
                if (!metaFile.exists())
                    return null
                if (!metaFile.name.endsWith(META_INFO_POSTFIX))
                    return fromFile(filesystemBasePath, metaFile)
                var pair: LocalStorageFilePair? = null
                try {
                    val reader = metaFile.bufferedReader()
                    val metaInfo: CommonMetaInfo = jackson.readValue(reader)
                    val pathString = Path(filesystemBasePath.pathString, metaInfo.path).pathString
                    val file = File(pathString)
                    if (!file.exists()) {
                        metaFile.delete()
                    } else {
                        pair = LocalStorageFilePair(
                            file = file,
                            metaFile = metaFile,
                            meta = metaInfo
                        )
                    }
                } catch (e: JacksonException) {
                    metaFile.delete()
                }
                return pair
            }

            fun from(filesystemBasePath: Path, anyFile: File): LocalStorageFilePair? {
                return if (anyFile.name.endsWith(META_INFO_POSTFIX))
                    fromMetaFile(filesystemBasePath, anyFile)
                else
                    fromFile(filesystemBasePath, anyFile)
            }

            fun from(filesystemBasePath: Path, storagePath: String): LocalStorageFilePair? {
                val filePath = Path(filesystemBasePath.pathString, storagePath)
                return from(filesystemBasePath, filePath.toFile())
            }
        }
    }

    /**
     * Перебирает все файлы и каталоги в relativePath и возвращает с мета-информацией
     *
     */
    private suspend fun scanStorage(
        baseStoragePath: String,
        maxDepth: Int,
        fileCallback: (suspend (File, CommonFile) -> Unit)? = null,
        dirCallback: (suspend (File, CommonDirectory) -> Unit)? = null
    ) {
        if (!checkAvailable())
            throw WallencException.Storage.NotAvailable()
        val basePath = Path(_filesystemBasePath.pathString, baseStoragePath)
        val workedFiles = mutableSetOf<String>()
        val workedMetaFiles = mutableSetOf<String>()

        scanFileSystem(basePath.toFile(), maxDepth, { file ->
            // Если парный файл уже был обработан - скип. Это позволит не читать metaFile дважды
            if (workedFiles.contains(file.absolutePath) || workedMetaFiles.contains(file.absolutePath)) {
                return@scanFileSystem
            }

            val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
            if (pair != null) {
                workedFiles.add(pair.file.absolutePath)
                workedMetaFiles.add(pair.metaFile.absolutePath)

                if (pair.file.isFile) {
                    fileCallback?.invoke(pair.file, CommonFile(pair.meta))
                } else {
                    dirCallback?.invoke(pair.file, CommonDirectory(pair.meta, null))
                }
            }
        })
    }


    /**
     * Считает файлы и их размер. Не бросает исключения, если файлы недоступны
     * @throws none Если возникла ошибка, оставляет размер и количества файлов равными null
     */
    private suspend fun scanSizeAndNumOfFiles() = withContext(ioDispatcher) {
        if (!checkAvailable()) {
            _size.value = null
            _numberOfFiles.value = null
            return@withContext
        }

        var size = 0L
        var numOfFiles = 0

        scanStorage(baseStoragePath = "/", maxDepth = -1, fileCallback = { _, commonFile ->
            size += commonFile.metaInfo.size
            numOfFiles++

            if (numOfFiles % DATA_PAGE_LENGTH == 0) {
                _size.value = size
                _numberOfFiles.value = numOfFiles
            }
        })

        _size.value = size
        _numberOfFiles.value = numOfFiles
    }

    override suspend fun getAllFiles(): List<IFile> = withContext(ioDispatcher) {
        if (!checkAvailable())
            return@withContext listOf()

        val list = mutableListOf<IFile>()
        scanStorage(baseStoragePath = "/", maxDepth = -1, fileCallback = { _, commonFile ->
            list.add(commonFile)
        })
        return@withContext list
    }

    override suspend fun getFiles(path: String): List<IFile> = withContext(ioDispatcher) {
        if (!checkAvailable())
            return@withContext listOf()

        val list = mutableListOf<IFile>()
        scanStorage(baseStoragePath = path, maxDepth = 0, fileCallback = { _, commonFile ->
            list.add(commonFile)
        })
        return@withContext list
    }

    override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = flow {
        if (!checkAvailable())
            return@flow

        val buf = mutableListOf<IFile>()
        var pageNumber = 0
        scanStorage(baseStoragePath = path, maxDepth = 0, fileCallback = { _, commonFile ->
            if (buf.size == DATA_PAGE_LENGTH) {
                val page = DataPage(
                    list = buf.toList(),
                    isLoading = false,
                    isError = false,
                    hasNext = true,
                    pageLength = DATA_PAGE_LENGTH,
                    pageIndex = pageNumber++
                )
                emit(page)
                buf.clear()
            }
            buf.add(commonFile)
        })
        // отправка последней страницы
        val page = DataPage(
            list = buf.toList(),
            isLoading = false,
            isError = false,
            hasNext = false,
            pageLength = DATA_PAGE_LENGTH,
            pageIndex = pageNumber++
        )
        emit(page)
    }.flowOn(ioDispatcher)

    override suspend fun getAllDirs(): List<IDirectory> = withContext(ioDispatcher) {
        if (!checkAvailable())
            return@withContext listOf()

        val list = mutableListOf<IDirectory>()
        scanStorage(baseStoragePath = "/", maxDepth = -1, dirCallback = { _, localDir ->
            list.add(localDir)
        })
        return@withContext list
    }

    override suspend fun getDirs(path: String): List<IDirectory> = withContext(ioDispatcher) {
        if (!checkAvailable())
            return@withContext listOf()

        val list = mutableListOf<IDirectory>()
        scanStorage(baseStoragePath = path, maxDepth = 0, dirCallback = { _, localDir ->
            list.add(localDir)
        })
        return@withContext list
    }

    override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = flow {
        if (!checkAvailable())
            return@flow

        val buf = mutableListOf<IDirectory>()
        var pageNumber = 0
        scanStorage(baseStoragePath = path, maxDepth = 0, dirCallback = { _, localDir ->
            if (buf.size == DATA_PAGE_LENGTH) {
                val page = DataPage(
                    list = buf.toList(),
                    isLoading = false,
                    isError = false,
                    hasNext = true,
                    pageLength = DATA_PAGE_LENGTH,
                    pageIndex = pageNumber++
                )
                emit(page)
                buf.clear()
            }
            buf.add(localDir)
        })
        // отправка последней страницы
        val page = DataPage(
            list = buf.toList(),
            isLoading = false,
            isError = false,
            hasNext = false,
            pageLength = DATA_PAGE_LENGTH,
            pageIndex = pageNumber++
        )
        emit(page)
    }.flowOn(ioDispatcher)

    override suspend fun getFileInfo(path: String): IFile {
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.UnexpectedState()
        return CommonFile(
            metaInfo = pair.meta,
        )
    }

    override suspend fun getDirInfo(path: String): IDirectory {
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.UnexpectedState()
        return CommonDirectory(
            metaInfo = pair.meta,
            elementsCount = null
        )
    }

    override suspend fun setHidden(path: String, hidden: Boolean) {
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.UnexpectedState()
        if (pair.meta.isHidden == hidden)
            return
        val newMeta = pair.meta.copy(isHidden = hidden)
        writeMeta(pair.metaFile, newMeta)
        _filesUpdates.emit(
            DataPage(
                list = listOf(
                    CommonFile(
                        metaInfo = newMeta
                    )
                ),
                pageLength = 1,
                pageIndex = 0
            )
        )
    }


    private fun writeMeta(metaFile: File, meta: IMetaInfo) {
        jackson.writeValue(metaFile, meta)
    }

    private suspend fun createFile(storagePath: String): CommonFile = withContext(ioDispatcher) {
        val path = Path(_filesystemBasePath.pathString, storagePath)
        val file = path.toFile()
        if (file.exists() && file.isDirectory)  {
            throw WallencException.Storage.UnexpectedState()
        } else if(!file.exists()) {
            val parent = Path(storagePath).parent
            createDir(parent.pathString)
            file.createNewFile()

            val cur = _numberOfFiles.value
            _numberOfFiles.value = if (cur == null) null else cur + 1
        }

        val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
            ?: throw WallencException.Storage.UnexpectedState()
        val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant(), size = Files.size(pair.file.toPath()))
        writeMeta(pair.metaFile, newMeta)
        _filesUpdates.emit(
            DataPage(
                list = listOf(CommonFile(pair.meta)),
                pageLength = 1,
                pageIndex = 0
            )
        )
        return@withContext CommonFile(newMeta)
    }

    private suspend fun createDir(storagePath: String): CommonDirectory = withContext(ioDispatcher) {
        val path = Path(_filesystemBasePath.pathString, storagePath)
        val file = path.toFile()
        if (file.exists() && !file.isDirectory) {
            throw WallencException.Storage.UnexpectedState()
        } else if(!file.exists()) {
            Files.createDirectories(path)
        }

        val pair = LocalStorageFilePair.from(_filesystemBasePath, file)
            ?: throw WallencException.Storage.UnexpectedState()
        val newMeta = pair.meta.copy(lastModified = Clock.systemUTC().instant())
        writeMeta(pair.metaFile, newMeta)
        _dirsUpdates.emit(
            DataPage(
                list = listOf(CommonDirectory(pair.meta, null)),
                pageLength = 1,
                pageIndex = 0
            )
        )
        return@withContext CommonDirectory(newMeta, 0)
    }

    override suspend fun touchFile(path: String): Unit = withContext(ioDispatcher) {
        touchFileInternal(path, recordJournal = true)
    }

    private suspend fun touchFileInternal(path: String, recordJournal: Boolean) {
        createFile(path)

        if (recordJournal) {
            appendSyncEntry(path = path, operation = StorageSyncOperation.UPSERT)
        }

        // перебор все каталогов и обновление их времени модификации
        var parent = Path(path).parent
        while(parent != null) {
           touchDir(parent.pathString)
           parent = parent.parent
        }
    }

    override suspend fun touchDir(path: String): Unit = withContext(ioDispatcher) {
        createDir(path)
    }

    override suspend fun delete(path: String, recordSyncJournal: Boolean) = withContext(ioDispatcher) {
        if (path == "/" || path.isBlank()) {
            throw WallencException.Storage.DeleteRootForbidden()
        }
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
        if (pair != null) {
            if (pair.file.isDirectory) pair.file.deleteRecursively()
            else pair.file.delete()
            pair.metaFile.delete()
            scanSizeAndNumOfFiles()
            if (recordSyncJournal) {
                appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
            }
        }
    }

    override suspend fun openWrite(path: String, recordSyncJournal: Boolean): OutputStream = withContext(ioDispatcher) {
        touchFileInternal(path, recordJournal = false)
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.FileNotFound()
        return@withContext pair.file.outputStream().onClosed {
            CoroutineScope(ioDispatcher).launch {
                touchFileInternal(path, recordJournal = false)
                scanSizeAndNumOfFiles()
                if (recordSyncJournal) {
                    appendSyncEntry(path = path, operation = StorageSyncOperation.UPSERT)
                }
            }
        }
    }

    override suspend fun openRead(path: String): InputStream = withContext(ioDispatcher) {
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.FileNotFound()
        return@withContext pair.file.inputStream()
    }

    override suspend fun moveToTrash(path: String, recordSyncJournal: Boolean) = withContext(ioDispatcher) {
        val pair = LocalStorageFilePair.from(_filesystemBasePath, path)
            ?: throw WallencException.Storage.FileNotFound()
        val newMeta = pair.meta.copy(isDeleted = true)
        writeMeta(pair.metaFile, newMeta)
        if (recordSyncJournal) {
            appendSyncEntry(path = path, operation = StorageSyncOperation.TRASH)
        }
    }

    override suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) {
        val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
        val path = dirPath.resolve(name)
        val file = path.toFile()
        if (!file.exists()) {
            throw WallencException.Storage.FileNotFound()
        }
        return@withContext file.inputStream()
    }

    override suspend fun openWriteSystemFile(name: String): OutputStream = withContext(ioDispatcher) {
        val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
        val path = dirPath.resolve(name)
        val file = path.toFile()
        if(!file.exists()) {
            Files.createDirectories(dirPath)
            file.createNewFile()
        }

        return@withContext file.outputStream()
    }

    override suspend fun readSyncJournal(): StorageSyncJournal = withContext(ioDispatcher) {
        journalBuffer.flushPending()
        readSyncJournalUnchecked()
    }

    override suspend fun flushPendingSyncJournal() = withContext(ioDispatcher) {
        journalBuffer.flushPending()
    }

    override suspend fun putSyncJournalEntries(entries: StorageSyncJournal) = withContext(ioDispatcher) {
        journalBuffer.putEntries(entries)
    }

    private suspend fun readSyncJournalUnchecked(): StorageSyncJournal {
        val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_JOURNAL_FILENAME) }
        return StorageSyncJournalCodec.read(jackson, bytes)
    }

    private suspend fun writeSyncJournal(journal: StorageSyncJournal) {
        openWriteSystemFile(SYNC_JOURNAL_FILENAME).use { out ->
            jackson.writeValue(out, journal)
        }
    }

    override suspend fun readSyncLock(): StorageSyncLock? = withContext(ioDispatcher) {
        val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_LOCK_FILENAME) }
        if (bytes.isEmpty()) {
            return@withContext null
        }
        return@withContext runCatching {
            jackson.readValue<StorageSyncLock>(bytes)
        }.getOrNull()
    }

    override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean = withContext(ioDispatcher) {
        val lockPath = systemLockPath()
        Files.createDirectories(lockPath.parent)
        FileChannel.open(
            lockPath,
            StandardOpenOption.CREATE,
            StandardOpenOption.READ,
            StandardOpenOption.WRITE,
        ).use { channel ->
            val fileLock = channel.lock()
            try {
                val current = readSyncLockFromChannel(channel)
                val now = Instant.now()
                val foreignLockActive = current != null &&
                    current.holderId != holderId &&
                    current.leaseUntil.isAfter(now) &&
                    current.updatedAt.plusSeconds(SYNC_LOCK_STALE_TIMEOUT_SECONDS).isAfter(now)
                if (foreignLockActive) {
                    return@withContext false
                }

                writeSyncLockToChannel(
                    channel = channel,
                    lock = StorageSyncLock(
                        holderId = holderId,
                        leaseUntil = leaseUntil,
                        updatedAt = now,
                    ),
                )
                return@withContext true
            } finally {
                fileLock.release()
            }
        }
    }

    override suspend fun releaseSyncLock(holderId: String) = withContext(ioDispatcher) {
        val lockPath = systemLockPath()
        Files.createDirectories(lockPath.parent)
        FileChannel.open(
            lockPath,
            StandardOpenOption.CREATE,
            StandardOpenOption.READ,
            StandardOpenOption.WRITE,
        ).use { channel ->
            val fileLock = channel.lock()
            try {
                val current = readSyncLockFromChannel(channel) ?: return@withContext
                if (current.holderId != holderId) {
                    return@withContext
                }
                channel.truncate(0)
                channel.force(true)
            } finally {
                fileLock.release()
            }
        }
    }

    override suspend fun forceClearSyncLock() = withContext(ioDispatcher) {
        val lockPath = systemLockPath()
        if (!Files.exists(lockPath)) {
            return@withContext
        }
        Files.createDirectories(lockPath.parent)
        FileChannel.open(
            lockPath,
            StandardOpenOption.READ,
            StandardOpenOption.WRITE,
        ).use { channel ->
            val fileLock = channel.lock()
            try {
                channel.truncate(0)
                channel.force(true)
            } finally {
                fileLock.release()
            }
        }
    }

    private fun systemLockPath(): Path =
        _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME).resolve(SYNC_LOCK_FILENAME)

    private fun readSyncLockFromChannel(channel: FileChannel): StorageSyncLock? {
        channel.position(0)
        val size = channel.size().toInt()
        if (size <= 0) {
            return null
        }
        val buffer = ByteBuffer.allocate(size)
        while (buffer.hasRemaining()) {
            val read = channel.read(buffer)
            if (read <= 0) break
        }
        val bytes = buffer.array().copyOf(buffer.position())
        if (bytes.isEmpty()) {
            return null
        }
        return runCatching { jackson.readValue(bytes, StorageSyncLock::class.java) }.getOrNull()
    }

    private fun writeSyncLockToChannel(channel: FileChannel, lock: StorageSyncLock) {
        val bytes = jackson.writeValueAsBytes(lock)
        channel.truncate(0)
        channel.position(0)
        channel.write(ByteBuffer.wrap(bytes))
        channel.force(true)
    }

    private suspend fun appendSyncEntry(path: String, operation: StorageSyncOperation) {
        val cleanedPath = StorageSyncPaths.normalize(path)
        if (!StorageSyncPaths.isSyncableUserPath(cleanedPath)) {
            return
        }
        val sequence = journalBuffer.nextSequence()
        val entry = journalBuffer.buildEntry(cleanedPath, operation, sequence)
        journalBuffer.appendEntry(cleanedPath, entry)
    }

    companion object {
        // Файлы, которые можно использовать для чтения и записи, но не отображаются в хранилище
        private const val SYSTEM_HIDDEN_DIRNAME = "wallenc-local-storage-meta-dir"
        private const val META_INFO_POSTFIX = ".wallenc-meta"
        private const val DATA_PAGE_LENGTH = 10
        private const val SYNC_JOURNAL_FILENAME = "sync-journal.json"
        private const val SYNC_LOCK_FILENAME = "sync-lock.json"
        private const val SYNC_LOCK_STALE_TIMEOUT_SECONDS: Long = 60 * 60
        private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
    }
}