import Foundation

/// Synor Database SDK client for Swift.
///
/// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores.
///
/// Example:
/// ```swift
/// let db = SynorDatabase(config: DatabaseConfig(apiKey: "your-api-key"))
///
/// // Key-Value operations
/// try await db.kv.set(key: "mykey", value: "myvalue", ttl: 3600)
/// let value = try await db.kv.get(key: "mykey")
///
/// // Document operations
/// let docId = try await db.documents.create(collection: "users", document: ["name": "Alice", "age": 30])
/// let doc = try await db.documents.get(collection: "users", id: docId)
///
/// // Vector operations
/// try await db.vectors.upsert(collection: "embeddings", vectors: [
///     VectorEntry(id: "vec1", vector: [0.1, 0.2, 0.3], metadata: ["label": "test"])
/// ])
///
/// // Time series operations
/// try await db.timeseries.write(series: "metrics", points: [
///     DataPoint(timestamp: Date().timeIntervalSince1970, value: 42.0)
/// ])
/// ```
public class SynorDatabase {
    private let config: DatabaseConfig
    private let session: URLSession
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder
    private var closed = false

    /// Key-Value store operations.
    public private(set) lazy var kv = KeyValueStore(db: self)

    /// Document store operations.
    public private(set) lazy var documents = DocumentStore(db: self)

    /// Vector store operations.
    public private(set) lazy var vectors = VectorStore(db: self)

    /// Time series store operations.
    public private(set) lazy var timeseries = TimeSeriesStore(db: self)

    public init(config: DatabaseConfig) {
        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: - Key-Value Store

    /// Key-Value store for simple key-value operations.
    public class KeyValueStore {
        private let db: SynorDatabase

        internal init(db: SynorDatabase) {
            self.db = db
        }

        /// Get a value by key.
        public func get(key: String) async throws -> Any? {
            let response: KVGetResponse = try await db.request(
                method: "GET",
                path: "/kv/\(key.urlEncoded)"
            )
            return response.value?.value
        }

        /// Set a value for a key with optional TTL.
        public func set(key: String, value: Any, ttl: Int? = nil) async throws {
            var body: [String: AnyCodable] = [
                "key": AnyCodable(key),
                "value": AnyCodable(value)
            ]
            if let ttl = ttl {
                body["ttl"] = AnyCodable(ttl)
            }
            let _: EmptyResponse = try await db.request(
                method: "PUT",
                path: "/kv/\(key.urlEncoded)",
                body: body
            )
        }

        /// Delete a key.
        public func delete(key: String) async throws {
            let _: EmptyResponse = try await db.request(
                method: "DELETE",
                path: "/kv/\(key.urlEncoded)"
            )
        }

        /// List keys by prefix.
        public func list(prefix: String) async throws -> [KeyValue] {
            let response: KVListResponse = try await db.request(
                method: "GET",
                path: "/kv?prefix=\(prefix.urlEncoded)"
            )
            return response.items ?? []
        }
    }

    // MARK: - Document Store

    /// Document store for document-oriented operations.
    public class DocumentStore {
        private let db: SynorDatabase

        internal init(db: SynorDatabase) {
            self.db = db
        }

        /// Create a new document.
        public func create(collection: String, document: [String: Any]) async throws -> String {
            let body = document.mapValues { AnyCodable($0) }
            let response: CreateDocumentResponse = try await db.request(
                method: "POST",
                path: "/collections/\(collection.urlEncoded)/documents",
                body: body
            )
            return response.id
        }

        /// Get a document by ID.
        public func get(collection: String, id: String) async throws -> Document {
            return try await db.request(
                method: "GET",
                path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
            )
        }

        /// Update a document.
        public func update(collection: String, id: String, update: [String: Any]) async throws {
            let body = update.mapValues { AnyCodable($0) }
            let _: EmptyResponse = try await db.request(
                method: "PATCH",
                path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)",
                body: body
            )
        }

        /// Delete a document.
        public func delete(collection: String, id: String) async throws {
            let _: EmptyResponse = try await db.request(
                method: "DELETE",
                path: "/collections/\(collection.urlEncoded)/documents/\(id.urlEncoded)"
            )
        }

        /// Query documents.
        public func query(collection: String, query: DocumentQuery) async throws -> [Document] {
            let response: DocumentListResponse = try await db.request(
                method: "POST",
                path: "/collections/\(collection.urlEncoded)/query",
                body: query
            )
            return response.documents ?? []
        }
    }

    // MARK: - Vector Store

    /// Vector store for similarity search operations.
    public class VectorStore {
        private let db: SynorDatabase

        internal init(db: SynorDatabase) {
            self.db = db
        }

        /// Upsert vectors into a collection.
        public func upsert(collection: String, vectors: [VectorEntry]) async throws {
            let body: [String: [VectorEntry]] = ["vectors": vectors]
            let _: EmptyResponse = try await db.request(
                method: "POST",
                path: "/vectors/\(collection.urlEncoded)/upsert",
                body: body
            )
        }

        /// Search for similar vectors.
        public func search(collection: String, vector: [Double], k: Int) async throws -> [SearchResult] {
            let body: [String: AnyCodable] = [
                "vector": AnyCodable(vector),
                "k": AnyCodable(k)
            ]
            let response: SearchListResponse = try await db.request(
                method: "POST",
                path: "/vectors/\(collection.urlEncoded)/search",
                body: body
            )
            return response.results ?? []
        }

        /// Delete vectors by IDs.
        public func delete(collection: String, ids: [String]) async throws {
            let body: [String: [String]] = ["ids": ids]
            let _: EmptyResponse = try await db.request(
                method: "DELETE",
                path: "/vectors/\(collection.urlEncoded)",
                body: body
            )
        }
    }

    // MARK: - Time Series Store

    /// Time series store for time-based data operations.
    public class TimeSeriesStore {
        private let db: SynorDatabase

        internal init(db: SynorDatabase) {
            self.db = db
        }

        /// Write data points to a series.
        public func write(series: String, points: [DataPoint]) async throws {
            let body: [String: [DataPoint]] = ["points": points]
            let _: EmptyResponse = try await db.request(
                method: "POST",
                path: "/timeseries/\(series.urlEncoded)/write",
                body: body
            )
        }

        /// Query data points from a series.
        public func query(series: String, range: TimeRange, aggregation: Aggregation? = nil) async throws -> [DataPoint] {
            var body: [String: AnyCodable] = ["range": AnyCodable(["start": range.start, "end": range.end])]
            if let aggregation = aggregation {
                body["aggregation"] = AnyCodable(["function": aggregation.function, "interval": aggregation.interval])
            }
            let response: DataPointListResponse = try await db.request(
                method: "POST",
                path: "/timeseries/\(series.urlEncoded)/query",
                body: body
            )
            return response.points ?? []
        }
    }

    // MARK: - Lifecycle

    /// Close the client.
    public func close() {
        closed = true
        session.invalidateAndCancel()
    }

    /// Check if the client is closed.
    public var isClosed: Bool { closed }

    /// Perform a health check.
    public func healthCheck() async -> Bool {
        do {
            let response: HealthResponse = try await request(method: "GET", path: "/health")
            return response.status == "healthy"
        } catch {
            return false
        }
    }

    // MARK: - Internal HTTP Methods

    internal func request<T: Decodable>(method: String, path: String) async throws -> T {
        return try await executeWithRetry {
            try await self.performRequest(method: method, path: path, body: nil as EmptyBody?)
        }
    }

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

    private func performRequest<T: Decodable, R: Encodable>(
        method: String,
        path: String,
        body: R?
    ) async throws -> T {
        if closed { throw DatabaseError.clientClosed }

        guard let url = URL(string: "\(config.endpoint)\(path)") else {
            throw DatabaseError.invalidResponse
        }

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

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

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

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

        if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
            if data.isEmpty && T.self == EmptyResponse.self {
                return EmptyResponse() as! T
            }
            do {
                return try decoder.decode(T.self, from: data)
            } catch {
                throw DatabaseError.decodingError(error)
            }
        }

        let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
        let code = errorInfo?["code"] as? String
        throw DatabaseError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
    }

    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 * (1 << attempt)))
                }
            }
        }

        throw lastError ?? DatabaseError.invalidResponse
    }
}

// MARK: - Private Helpers

private struct EmptyBody: Encodable {}
private struct EmptyResponse: Decodable {}

private extension String {
    var urlEncoded: String {
        addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
    }
}
