package com.syncflow.data.providers.sftp

import com.syncflow.data.providers.CloudProvider
import com.syncflow.data.security.CredentialStore
import com.syncflow.domain.model.CloudAccount
import com.syncflow.domain.model.RemoteFile
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.SFTPClient
import net.schmizz.sshj.xfer.InMemorySourceFile
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant

class SftpProvider(private val account: CloudAccount, private val credentialStore: CredentialStore) : CloudProvider {

    private val creds = Json.parseToJsonElement(account.credentialJson).jsonObject
    private val host = account.serverUrl ?: "localhost"
    private val port = account.port ?: 22
    private val username = creds["username"]?.jsonPrimitive?.content ?: ""
    private val password = creds["password"]?.jsonPrimitive?.content
    private val privateKey = creds["private_key"]?.jsonPrimitive?.content

    // Persistent SSH connection reused across all operations in the provider's lifetime.
    // Each call to withSftp checks liveness and reconnects if the connection dropped.
    // This eliminates the per-operation connect/auth/disconnect cycle that caused
    // 100+ SSH handshakes during a recursive directory listing + file-transfer sync,
    // leading to connection timeouts on large folder trees (e.g. 69 subdirectories).
    private var sshClient: SSHClient? = null

    private fun getOrCreateSsh(): SSHClient {
        val existing = sshClient
        if (existing != null && existing.isConnected && existing.isAuthenticated) return existing
        val ssh = SSHClient()
        ssh.addHostKeyVerifier(TofuHostKeyVerifier(credentialStore))
        ssh.connect(host, port)
        if (!privateKey.isNullOrBlank()) {
            ssh.authPublickey(username, ssh.loadKeys(privateKey, null, null))
        } else {
            ssh.authPassword(username, password ?: "")
        }
        sshClient = ssh
        return ssh
    }

    private fun <T> withSftp(block: (SFTPClient) -> T): T {
        return try {
            getOrCreateSsh().newSFTPClient().use(block)
        } catch (e: Exception) {
            // Connection may have gone stale — reset and retry once with a fresh connection.
            runCatching { sshClient?.disconnect() }
            sshClient = null
            getOrCreateSsh().newSFTPClient().use(block)
        }
    }

    override suspend fun testConnection(): Result<Unit> = runCatching { withSftp { it.ls("/") } }

    override suspend fun listFiles(remotePath: String): Result<List<RemoteFile>> = runCatching {
        withSftp { sftp ->
            sftp.ls(remotePath)
                // Drop "."/".." and any name with a path separator so a hostile server can't
                // smuggle a traversal segment into a local/remote path.
                .filter { it.name != "." && it.name != ".." && !it.name.contains('/') && !it.name.contains('\\') }
                .map { entry ->
                    RemoteFile(
                        path = "$remotePath/${entry.name}".replace("//", "/"),
                        name = entry.name,
                        isDirectory = entry.isDirectory,
                        sizeBytes = entry.attributes.size,
                        modifiedAt = Instant.ofEpochSecond(entry.attributes.mtime.toLong()),
                        etag = null,
                        mimeType = null,
                    )
                }
        }
    }

    override suspend fun uploadFile(
        localStream: InputStream,
        remotePath: String,
        sizeBytes: Long,
        onProgress: (Long) -> Unit,
    ): Result<RemoteFile> = runCatching {
        // Upload to a hidden temp sibling, then rename onto the destination so an interrupted
        // transfer never leaves a truncated file at the real path.
        val dir = remotePath.substringBeforeLast('/', "")
        val name = remotePath.substringAfterLast('/')
        val tmpPath = if (dir.isEmpty()) ".$name.sfpart" else "$dir/.$name.sfpart"
        withSftp { sftp ->
            sftp.put(object : InMemorySourceFile() {
                override fun getName() = name
                override fun getLength() = sizeBytes
                override fun getInputStream() = localStream
            }, tmpPath)
            // SFTP rename fails if the target exists on servers without the POSIX-rename
            // extension, so fall back to removing the destination first.
            try {
                sftp.rename(tmpPath, remotePath)
            } catch (e: Exception) {
                runCatching { sftp.rm(remotePath) }
                sftp.rename(tmpPath, remotePath)
            }
        }
        getFileMetadata(remotePath).getOrThrow()
    }

    override suspend fun downloadFile(remotePath: String, destination: OutputStream, onProgress: (Long) -> Unit): Result<Unit> = runCatching {
        val tmp = java.io.File.createTempFile("sf_", ".tmp")
        try {
            withSftp { sftp -> sftp.get(remotePath, tmp.absolutePath) }
            var total = 0L
            tmp.inputStream().use { src ->
                val buf = ByteArray(65536)
                var n: Int
                while (src.read(buf).also { n = it } != -1) { destination.write(buf, 0, n); total += n; onProgress(total) }
            }
        } finally {
            tmp.delete()
        }
    }

    override suspend fun deleteFile(remotePath: String): Result<Unit> = runCatching {
        withSftp { it.rm(remotePath) }
    }

    override suspend fun createDirectory(remotePath: String): Result<Unit> = runCatching {
        withSftp { it.mkdirs(remotePath) }
    }

    override suspend fun getFileMetadata(remotePath: String): Result<RemoteFile> = runCatching {
        withSftp { sftp ->
            val attr = sftp.stat(remotePath)
            RemoteFile(
                path = remotePath,
                name = remotePath.substringAfterLast('/'),
                isDirectory = attr.type.toString().contains("DIRECTORY"),
                sizeBytes = attr.size,
                modifiedAt = Instant.ofEpochSecond(attr.mtime.toLong()),
                etag = null,
                mimeType = null,
            )
        }
    }

    override suspend fun moveFile(fromPath: String, toPath: String): Result<Unit> = runCatching {
        withSftp { it.rename(fromPath, toPath) }
    }
}
