package io.synor.storage

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.json.Json
import java.io.Closeable

/**
 * Synor Storage SDK client for Kotlin.
 *
 * Provides IPFS-compatible decentralized storage with upload, download,
 * pinning, and CAR file operations.
 *
 * Example:
 * ```kotlin
 * val storage = SynorStorage(StorageConfig.builder("your-api-key").build())
 *
 * // Upload a file
 * val data = "Hello, World!".toByteArray()
 * val result = storage.upload(data, UploadOptions(name = "hello.txt"))
 * println("CID: ${result.cid}")
 *
 * // Download content
 * val content = storage.download(result.cid)
 * println("Content: ${String(content)}")
 *
 * // Pin content
 * val pin = storage.pin(PinRequest(cid = result.cid, durationDays = 30))
 * println("Pin status: ${pin.status}")
 *
 * storage.close()
 * ```
 */
class SynorStorage(private val config: StorageConfig) : Closeable {

    private val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        encodeDefaults = true
    }

    private val client = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(json)
        }

        install(HttpTimeout) {
            requestTimeoutMillis = config.timeout
            connectTimeoutMillis = config.timeout
        }

        defaultRequest {
            header("Authorization", "Bearer ${config.apiKey}")
        }
    }

    // Upload operations

    /**
     * Upload data to storage.
     *
     * @param data The data to upload
     * @param options Upload options
     * @return Upload response with CID
     */
    suspend fun upload(data: ByteArray, options: UploadOptions = UploadOptions()): UploadResponse {
        return executeWithRetry {
            val response = client.submitFormWithBinaryData(
                url = "${config.endpoint}/upload",
                formData = formData {
                    append("file", data, Headers.build {
                        append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
                        append(HttpHeaders.ContentDisposition, "filename=\"${options.name ?: "file"}\"")
                    })
                    options.name?.let { append("name", it) }
                    append("wrap_with_directory", options.wrapWithDirectory.toString())
                    append("hash_algorithm", options.hashAlgorithm.name.lowercase().replace("_", "-"))
                    options.chunkSize?.let { append("chunk_size", it.toString()) }
                    options.pinDurationDays?.let { append("pin_duration_days", it.toString()) }
                }
            )
            handleResponse(response)
        }
    }

    /**
     * Upload multiple files as a directory.
     *
     * @param files List of file entries
     * @param dirName Optional directory name
     * @return Upload response with root CID
     */
    suspend fun uploadDirectory(files: List<FileEntry>, dirName: String? = null): UploadResponse {
        return executeWithRetry {
            val response = client.submitFormWithBinaryData(
                url = "${config.endpoint}/upload/directory",
                formData = formData {
                    files.forEach { file ->
                        append("files", file.content, Headers.build {
                            append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
                            append(HttpHeaders.ContentDisposition, "filename=\"${file.path}\"")
                        })
                    }
                    dirName?.let { append("name", it) }
                }
            )
            handleResponse(response)
        }
    }

    // Download operations

    /**
     * Download content by CID.
     *
     * @param cid The content identifier
     * @return The content as bytes
     */
    suspend fun download(cid: String): ByteArray {
        return executeWithRetry {
            val response = client.get("${config.endpoint}/content/$cid")
            if (response.status.isSuccess()) {
                response.body()
            } else {
                throw StorageException(
                    "Failed to download content: ${response.status}",
                    statusCode = response.status.value
                )
            }
        }
    }

    /**
     * Download content as a stream.
     *
     * @param cid The content identifier
     * @return Flow of byte chunks
     */
    fun downloadStream(cid: String): Flow<ByteArray> = flow {
        val response = client.get("${config.endpoint}/content/$cid")
        if (!response.status.isSuccess()) {
            throw StorageException(
                "Failed to download content: ${response.status}",
                statusCode = response.status.value
            )
        }

        val channel: ByteReadChannel = response.body()
        val buffer = ByteArray(8192)
        while (!channel.isClosedForRead) {
            val bytesRead = channel.readAvailable(buffer)
            if (bytesRead > 0) {
                emit(buffer.copyOf(bytesRead))
            }
        }
    }

    /**
     * Get the gateway URL for content.
     *
     * @param cid The content identifier
     * @param path Optional path within directory
     * @return Gateway URL
     */
    fun getGatewayUrl(cid: String, path: String? = null): String {
        return if (path != null) {
            "${config.gateway}/ipfs/$cid/$path"
        } else {
            "${config.gateway}/ipfs/$cid"
        }
    }

    // Pinning operations

    /**
     * Pin content to ensure availability.
     *
     * @param request Pin request
     * @return Pin status
     */
    suspend fun pin(request: PinRequest): Pin {
        return post("/pins", request)
    }

    /**
     * Unpin content.
     *
     * @param cid The content identifier
     */
    suspend fun unpin(cid: String) {
        delete("/pins/$cid")
    }

    /**
     * Get pin status.
     *
     * @param cid The content identifier
     * @return Pin information
     */
    suspend fun getPinStatus(cid: String): Pin {
        return get("/pins/$cid")
    }

    /**
     * List all pins.
     *
     * @param status Optional status filter
     * @param limit Maximum number of results
     * @param offset Pagination offset
     * @return List of pins
     */
    suspend fun listPins(
        status: PinStatus? = null,
        limit: Int = 50,
        offset: Int = 0
    ): List<Pin> {
        val query = buildString {
            append("?limit=$limit&offset=$offset")
            status?.let { append("&status=${it.name.lowercase()}") }
        }
        return get("/pins$query")
    }

    // CAR file operations

    /**
     * Create a CAR file from entries.
     *
     * @param entries List of entries
     * @return CAR file information
     */
    suspend fun createCar(entries: List<CarEntry>): CarFile {
        return executeWithRetry {
            val response = client.submitFormWithBinaryData(
                url = "${config.endpoint}/car",
                formData = formData {
                    entries.forEach { entry ->
                        append("files", entry.content, Headers.build {
                            append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
                            append(HttpHeaders.ContentDisposition, "filename=\"${entry.path}\"")
                        })
                    }
                }
            )
            handleResponse(response)
        }
    }

    /**
     * Import a CAR file.
     *
     * @param carData CAR file bytes
     * @return List of imported CIDs
     */
    suspend fun importCar(carData: ByteArray): List<String> {
        return executeWithRetry {
            val response = client.submitFormWithBinaryData(
                url = "${config.endpoint}/car/import",
                formData = formData {
                    append("file", carData, Headers.build {
                        append(HttpHeaders.ContentType, ContentType.Application.OctetStream)
                        append(HttpHeaders.ContentDisposition, "filename=\"archive.car\"")
                    })
                }
            )
            val result: Map<String, List<String>> = handleResponse(response)
            result["cids"] ?: emptyList()
        }
    }

    /**
     * Export content as a CAR file.
     *
     * @param cid The content identifier
     * @return CAR file bytes
     */
    suspend fun exportCar(cid: String): ByteArray {
        return executeWithRetry {
            val response = client.get("${config.endpoint}/car/$cid")
            if (response.status.isSuccess()) {
                response.body()
            } else {
                throw StorageException(
                    "Failed to export CAR: ${response.status}",
                    statusCode = response.status.value
                )
            }
        }
    }

    // Directory operations

    /**
     * List directory contents.
     *
     * @param cid The directory CID
     * @return List of entries
     */
    suspend fun listDirectory(cid: String): List<DirectoryEntry> {
        return get("/content/$cid/ls")
    }

    // Statistics

    /**
     * Get storage statistics.
     *
     * @return Storage stats
     */
    suspend fun getStats(): StorageStats {
        return get("/stats")
    }

    private suspend inline fun <reified T> get(path: String): T {
        return executeWithRetry {
            val response = client.get("${config.endpoint}$path") {
                header("Content-Type", "application/json")
            }
            handleResponse(response)
        }
    }

    private suspend inline fun <reified T, reified R> post(path: String, body: R): T {
        return executeWithRetry {
            val response = client.post("${config.endpoint}$path") {
                header("Content-Type", "application/json")
                setBody(body)
            }
            handleResponse(response)
        }
    }

    private suspend fun delete(path: String) {
        executeWithRetry {
            val response = client.delete("${config.endpoint}$path")
            if (!response.status.isSuccess()) {
                throw StorageException(
                    "Delete failed: ${response.status}",
                    statusCode = response.status.value
                )
            }
        }
    }

    private suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
        if (response.status.isSuccess()) {
            return response.body()
        }

        val errorBody = try {
            response.bodyAsText()
        } catch (e: Exception) {
            "Unknown error"
        }

        throw StorageException(
            message = "Storage API error: $errorBody",
            statusCode = response.status.value
        )
    }

    private suspend inline fun <T> executeWithRetry(block: () -> T): T {
        var lastException: Exception? = null

        repeat(config.retries) { attempt ->
            try {
                return block()
            } catch (e: Exception) {
                lastException = e
                if (config.debug) {
                    println("Attempt ${attempt + 1} failed: ${e.message}")
                }
                if (attempt < config.retries - 1) {
                    delay(1000L * (attempt + 1))
                }
            }
        }

        throw lastException ?: StorageException("Unknown error after ${config.retries} retries")
    }

    override fun close() {
        client.close()
    }
}
