package invalid.lena.rsend

import java.io.File
import java.nio.file.Files

// FolderRules holds the small set of path checks shared by the editor and the
// worker. Remote paths are compared lexically because their filesystem lives
// on another machine and cannot be resolved safely on the phone.
object FolderRules {
    private const val MAX_NAME_CHARS = 256
    private const val MAX_PATH_CHARS = 4096
    private const val MAX_EXCLUDE_CHARS = 512
    internal const val MAX_EXCLUDES = 64

    fun nameAllowed(value: String): Boolean =
        value.length <= MAX_NAME_CHARS && value.none(Char::isISOControl)

    fun localPathAllowed(path: String): Boolean {
        if (path.isEmpty() || path.length > MAX_PATH_CHARS || path.any(Char::isISOControl)) return false
        val file = File(path)
        if (!file.isAbsolute) return false
        return try {
            file.toPath().normalize() != File(File.separator).toPath()
        } catch (_: Exception) {
            false
        }
    }

    // Empty directories are readable sources. Opening a DirectoryStream checks
    // access without allocating one String for every entry in a large media
    // directory. The source itself must not be a symlink: rsync follows a
    // command-line symlink when the source has a trailing slash.
    fun sourceReadable(path: String): Boolean {
        if (!localPathAllowed(path)) return false
        val file = File(path)
        return try {
            if (!file.isDirectory || Files.isSymbolicLink(file.toPath())) return false
            Files.newDirectoryStream(file.toPath()).use { true }
        } catch (_: Exception) {
            false
        }
    }

    fun remotePathAllowed(path: String): Boolean {
        if (path.isEmpty() || path.length > MAX_PATH_CHARS || path.startsWith(':') ||
            path.any(Char::isISOControl)
        ) return false
        if (path.startsWith('~') && !path.startsWith("~/")) return false
        if (path.split('/').any { it == "." || it == ".." }) return false
        return remotePath(path).parts.isNotEmpty()
    }

    fun excludeAllowed(pattern: String): Boolean =
        pattern.isNotEmpty() && pattern.length <= MAX_EXCLUDE_CHARS && pattern.none(Char::isISOControl)

    fun excludesAllowed(patterns: List<String>): Boolean =
        patterns.size <= MAX_EXCLUDES && patterns.all(::excludeAllowed)

    fun destinationConflict(cfg: Config, candidate: Folder, exceptIndex: Int): Folder? {
        val candidateRemote = cfg.remote(candidate.remoteName)
        return cfg.folders.withIndex().firstOrNull { (i, existing) ->
            i != exceptIndex &&
                sameEndpoint(candidate.remoteName, candidateRemote, existing.remoteName, cfg.remote(existing.remoteName)) &&
                remotePathsConflict(candidate.remotePath, existing.remotePath)
        }?.value
    }

    fun firstDestinationConflict(cfg: Config): Pair<Folder, Folder>? {
        for ((i, folder) in cfg.folders.withIndex()) {
            val other = destinationConflict(cfg, folder, i)
            if (other != null) return folder to other
        }
        return null
    }

    // Absolute and home-relative destinations can resolve to the same tree,
    // but the phone cannot prove how a remote account's home is laid out. Keep
    // one style per endpoint and compare conservatively on case too.
    internal fun remotePathsConflict(a: String, b: String): Boolean {
        val left = remotePath(a)
        val right = remotePath(b)
        if (left.absolute != right.absolute) return true
        val shorter = minOf(left.parts.size, right.parts.size)
        return (0 until shorter).all { left.parts[it].equals(right.parts[it], ignoreCase = true) }
    }

    private fun sameEndpoint(aName: String, a: Remote?, bName: String, b: Remote?): Boolean {
        if (aName == bName) return true
        if (a == null || b == null) return false
        return a.user == b.user && a.port == b.port && a.host.equals(b.host, ignoreCase = true)
    }

    private data class RemotePath(val absolute: Boolean, val parts: List<String>)

    private fun remotePath(path: String): RemotePath {
        val absolute = path.startsWith("/")
        var value = path
        if (!absolute) {
            value = when {
                value == "~" -> ""
                value.startsWith("~/") -> value.substring(2)
                else -> value
            }
        }
        val parts = value.split('/').filter(String::isNotEmpty)
        return RemotePath(absolute, parts)
    }
}
