import Foundation

/// Synor Storage SDK client for Swift.
///
/// Provides IPFS-compatible decentralized storage with upload, download,
/// pinning, and CAR file operations.
///
/// Example:
/// ```swift
/// let storage = SynorStorage(config: StorageConfig(apiKey: "your-api-key"))
///
/// // Upload a file
/// let data = "Hello, World!".data(using: .utf8)!
/// let result = try await storage.upload(data: data, options: UploadOptions(name: "hello.txt"))
/// print("CID: \(result.cid)")
///
/// // Download content
/// let content = try await storage.download(cid: result.cid)
/// print("Content: \(String(data: content, encoding: .utf8)!)")
///
/// // Pin content
/// let pin = try await storage.pin(request: PinRequest(cid: result.cid, durationDays: 30))
/// print("Pin status: \(pin.status)")
/// ```
public class SynorStorage {
    private let config: StorageConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder

    public init(config: StorageConfig) {
        self.config = config

        let sessionConfig = URLSessionConfiguration.default
        sessionConfig.timeoutIntervalForRequest = config.timeout
        sessionConfig.timeoutIntervalForResource = config.timeout * 2
        self.session = URLSession(configuration: sessionConfig)

        self.decoder = JSONDecoder()
        self.encoder = JSONEncoder()
    }

    // MARK: - Upload Operations

    /// Upload data to storage.
    public func upload(data: Data, options: UploadOptions = UploadOptions()) async throws -> UploadResponse {
        return try await executeWithRetry {
            try await self.performMultipartUpload(
                path: "/upload",
                data: data,
                filename: options.name ?? "file",
                additionalFields: self.buildUploadFields(options: options)
            )
        }
    }

    /// Upload multiple files as a directory.
    public func uploadDirectory(files: [FileEntry], dirName: String? = nil) async throws -> UploadResponse {
        return try await executeWithRetry {
            try await self.performMultipartDirectoryUpload(
                path: "/upload/directory",
                files: files,
                dirName: dirName
            )
        }
    }

    private func buildUploadFields(options: UploadOptions) -> [String: String] {
        var fields: [String: String] = [
            "wrap_with_directory": String(options.wrapWithDirectory),
            "hash_algorithm": options.hashAlgorithm.rawValue
        ]

        if let name = options.name {
            fields["name"] = name
        }
        if let chunkSize = options.chunkSize {
            fields["chunk_size"] = String(chunkSize)
        }
        if let pinDuration = options.pinDurationDays {
            fields["pin_duration_days"] = String(pinDuration)
        }

        return fields
    }

    // MARK: - Download Operations

    /// Download content by CID.
    public func download(cid: String) async throws -> Data {
        return try await executeWithRetry {
            try await self.performDownload(path: "/content/\(cid)")
        }
    }

    /// Download content as an async stream.
    public func downloadStream(cid: String) -> AsyncThrowingStream<Data, Error> {
        AsyncThrowingStream { continuation in
            Task {
                do {
                    guard let url = URL(string: "\(config.endpoint)/content/\(cid)") else {
                        continuation.finish(throwing: StorageError.invalidResponse)
                        return
                    }

                    var request = URLRequest(url: url)
                    request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")

                    let (bytes, response) = try await session.bytes(for: request)

                    guard let httpResponse = response as? HTTPURLResponse,
                          httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 else {
                        continuation.finish(throwing: StorageError.httpError(
                            statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0,
                            message: "Download failed"
                        ))
                        return
                    }

                    var buffer = Data()
                    let chunkSize = 8192

                    for try await byte in bytes {
                        buffer.append(byte)
                        if buffer.count >= chunkSize {
                            continuation.yield(buffer)
                            buffer = Data()
                        }
                    }

                    if !buffer.isEmpty {
                        continuation.yield(buffer)
                    }

                    continuation.finish()
                } catch {
                    continuation.finish(throwing: StorageError.networkError(error))
                }
            }
        }
    }

    /// Get the gateway URL for content.
    public func getGatewayUrl(cid: String, path: String? = nil) -> String {
        if let path = path {
            return "\(config.gateway)/ipfs/\(cid)/\(path)"
        }
        return "\(config.gateway)/ipfs/\(cid)"
    }

    // MARK: - Pinning Operations

    /// Pin content to ensure availability.
    public func pin(request: PinRequest) async throws -> Pin {
        return try await post(path: "/pins", body: request)
    }

    /// Unpin content.
    public func unpin(cid: String) async throws {
        try await delete(path: "/pins/\(cid)")
    }

    /// Get pin status.
    public func getPinStatus(cid: String) async throws -> Pin {
        return try await get(path: "/pins/\(cid)")
    }

    /// List all pins.
    public func listPins(status: PinStatus? = nil, limit: Int = 50, offset: Int = 0) async throws -> [Pin] {
        var path = "/pins?limit=\(limit)&offset=\(offset)"
        if let status = status {
            path += "&status=\(status.rawValue)"
        }
        return try await get(path: path)
    }

    // MARK: - CAR File Operations

    /// Create a CAR file from entries.
    public func createCar(entries: [CarEntry]) async throws -> CarFile {
        return try await executeWithRetry {
            let files = entries.map { FileEntry(path: $0.path, content: $0.content) }
            return try await self.performMultipartDirectoryUpload(
                path: "/car",
                files: files,
                dirName: nil
            )
        }
    }

    /// Import a CAR file.
    public func importCar(carData: Data) async throws -> [String] {
        let response: ImportCarResponse = try await executeWithRetry {
            try await self.performMultipartUpload(
                path: "/car/import",
                data: carData,
                filename: "archive.car",
                additionalFields: [:]
            )
        }
        return response.cids
    }

    /// Export content as a CAR file.
    public func exportCar(cid: String) async throws -> Data {
        return try await executeWithRetry {
            try await self.performDownload(path: "/car/\(cid)")
        }
    }

    // MARK: - Directory Operations

    /// List directory contents.
    public func listDirectory(cid: String) async throws -> [DirectoryEntry] {
        return try await get(path: "/content/\(cid)/ls")
    }

    // MARK: - Statistics

    /// Get storage statistics.
    public func getStats() async throws -> StorageStats {
        return try await get(path: "/stats")
    }

    // MARK: - HTTP Methods

    private func get<T: Decodable>(path: String) async throws -> T {
        return try await executeWithRetry {
            try await self.performRequest(method: "GET", path: path, body: nil as Empty?)
        }
    }

    private func post<T: Decodable, R: Encodable>(path: String, body: R) async throws -> T {
        return try await executeWithRetry {
            try await self.performRequest(method: "POST", path: path, body: body)
        }
    }

    private func delete(path: String) async throws {
        try await executeWithRetry {
            let _: EmptyResponse = try await self.performRequest(method: "DELETE", path: path, body: nil as Empty?)
        }
    }

    private func performRequest<T: Decodable, R: Encodable>(
        method: String,
        path: String,
        body: R?
    ) async throws -> T {
        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw StorageError.invalidResponse
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        if let body = body {
            do {
                request.httpBody = try encoder.encode(body)
            } catch {
                throw StorageError.encodingError(error)
            }
        }

        let (data, response): (Data, URLResponse)
        do {
            (data, response) = try await session.data(for: request)
        } catch {
            throw StorageError.networkError(error)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw StorageError.invalidResponse
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            // Handle empty response for DELETE
            if data.isEmpty && T.self == EmptyResponse.self {
                return EmptyResponse() as! T
            }

            do {
                return try decoder.decode(T.self, from: data)
            } catch {
                throw StorageError.decodingError(error)
            }
        }

        let message = String(data: data, encoding: .utf8) ?? "Unknown error"
        throw StorageError.httpError(statusCode: httpResponse.statusCode, message: message)
    }

    private func performDownload(path: String) async throws -> Data {
        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw StorageError.invalidResponse
        }

        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")

        let (data, response): (Data, URLResponse)
        do {
            (data, response) = try await session.data(for: request)
        } catch {
            throw StorageError.networkError(error)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw StorageError.invalidResponse
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            return data
        }

        let message = String(data: data, encoding: .utf8) ?? "Unknown error"
        throw StorageError.httpError(statusCode: httpResponse.statusCode, message: message)
    }

    private func performMultipartUpload<T: Decodable>(
        path: String,
        data: Data,
        filename: String,
        additionalFields: [String: String]
    ) async throws -> T {
        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw StorageError.invalidResponse
        }

        let boundary = UUID().uuidString

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        var body = Data()

        // Add file
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
        body.append(data)
        body.append("\r\n".data(using: .utf8)!)

        // Add additional fields
        for (key, value) in additionalFields {
            body.append("--\(boundary)\r\n".data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
            body.append("\(value)\r\n".data(using: .utf8)!)
        }

        body.append("--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = body

        let (responseData, response): (Data, URLResponse)
        do {
            (responseData, response) = try await session.data(for: request)
        } catch {
            throw StorageError.networkError(error)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw StorageError.invalidResponse
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            do {
                return try decoder.decode(T.self, from: responseData)
            } catch {
                throw StorageError.decodingError(error)
            }
        }

        let message = String(data: responseData, encoding: .utf8) ?? "Unknown error"
        throw StorageError.httpError(statusCode: httpResponse.statusCode, message: message)
    }

    private func performMultipartDirectoryUpload<T: Decodable>(
        path: String,
        files: [FileEntry],
        dirName: String?
    ) async throws -> T {
        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw StorageError.invalidResponse
        }

        let boundary = UUID().uuidString

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        var body = Data()

        // Add files
        for file in files {
            body.append("--\(boundary)\r\n".data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(file.path)\"\r\n".data(using: .utf8)!)
            body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
            body.append(file.content)
            body.append("\r\n".data(using: .utf8)!)
        }

        // Add directory name if provided
        if let dirName = dirName {
            body.append("--\(boundary)\r\n".data(using: .utf8)!)
            body.append("Content-Disposition: form-data; name=\"name\"\r\n\r\n".data(using: .utf8)!)
            body.append("\(dirName)\r\n".data(using: .utf8)!)
        }

        body.append("--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = body

        let (responseData, response): (Data, URLResponse)
        do {
            (responseData, response) = try await session.data(for: request)
        } catch {
            throw StorageError.networkError(error)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw StorageError.invalidResponse
        }

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            do {
                return try decoder.decode(T.self, from: responseData)
            } catch {
                throw StorageError.decodingError(error)
            }
        }

        let message = String(data: responseData, encoding: .utf8) ?? "Unknown error"
        throw StorageError.httpError(statusCode: httpResponse.statusCode, message: message)
    }

    private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
        var lastError: Error?

        for attempt in 0..<config.retries {
            do {
                return try await operation()
            } catch {
                lastError = error
                if config.debug {
                    print("Attempt \(attempt + 1) failed: \(error)")
                }
                if attempt < config.retries - 1 {
                    try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (attempt + 1)))
                }
            }
        }

        throw lastError ?? StorageError.invalidResponse
    }
}

// Helper types
private struct Empty: Encodable {}
private struct EmptyResponse: Decodable {}
