import Foundation
import CQuicProQuo

/// High-level quicproquo client for iOS/macOS.
///
/// Wraps ``libquicproquo_ffi`` to provide a Swift-native API for
/// connecting, authenticating, and messaging.
///
/// ```swift
/// let client = try QpqClient(
///     server: "127.0.0.1:5001",
///     caCertPath: "/path/to/ca.pem"
/// )
/// try client.login(username: "alice", password: "secret")
/// try client.send(to: "bob", message: "hello".data(using: .utf8)!)
/// let messages = try client.receive(timeoutMs: 5000)
/// client.disconnect()
/// ```
public final class QpqClient: @unchecked Sendable {
    private var handle: OpaquePointer?
    private let lock = NSLock()

    /// Connect to a quicproquo server.
    ///
    /// - Parameters:
    ///   - server: Server address as ``host:port``.
    ///   - caCertPath: Path to the PEM-encoded CA certificate.
    ///   - serverName: TLS SNI server name (defaults to host from *server*).
    /// - Throws: ``QpqError/connectionFailed(_:)`` if the connection fails.
    public init(server: String, caCertPath: String, serverName: String? = nil) throws {
        let sn = serverName ?? server.components(separatedBy: ":").first ?? server

        let h = qpq_connect(server, caCertPath, sn)
        guard let h else {
            throw QpqError.connectionFailed("qpq_connect returned NULL for \(server)")
        }
        self.handle = h
    }

    deinit {
        disconnect()
    }

    // MARK: - Authentication

    /// Authenticate with the server using OPAQUE (username + password).
    ///
    /// - Throws: ``QpqError/authFailed(_:)`` on bad credentials.
    public func login(username: String, password: String) throws {
        try withHandle { h in
            let code = qpq_login(h, username, password)
            try checkStatus(code, handle: h)
        }
    }

    // MARK: - Messaging

    /// Send a message to a recipient by username.
    ///
    /// The message is encrypted via MLS before delivery.
    ///
    /// - Parameters:
    ///   - recipient: Recipient username.
    ///   - message: Message payload (arbitrary bytes).
    public func send(to recipient: String, message: Data) throws {
        try withHandle { h in
            let code = message.withUnsafeBytes { buffer -> Int32 in
                guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
                    return QPQ_ERROR
                }
                return qpq_send(h, recipient, ptr, buffer.count)
            }
            try checkStatus(code, handle: h)
        }
    }

    /// Receive pending messages, blocking up to ``timeoutMs`` milliseconds.
    ///
    /// - Returns: An array of message strings (UTF-8).
    public func receive(timeoutMs: UInt32 = 5000) throws -> [String] {
        try withHandle { h in
            var jsonPtr: UnsafeMutablePointer<CChar>?
            let code = qpq_receive(h, timeoutMs, &jsonPtr)
            try checkStatus(code, handle: h)

            guard let jsonPtr else {
                return []
            }
            defer { qpq_free_string(jsonPtr) }

            let jsonString = String(cString: jsonPtr)

            guard let data = jsonString.data(using: .utf8),
                  let array = try? JSONSerialization.jsonObject(with: data) as? [String]
            else {
                return []
            }
            return array
        }
    }

    // MARK: - Lifecycle

    /// Disconnect from the server and release resources.
    public func disconnect() {
        lock.lock()
        defer { lock.unlock() }

        if let h = handle {
            qpq_disconnect(h)
            handle = nil
        }
    }

    /// Whether the client is currently connected.
    public var isConnected: Bool {
        lock.lock()
        defer { lock.unlock() }
        return handle != nil
    }

    // MARK: - Internal

    private func withHandle<T>(_ body: (OpaquePointer) throws -> T) throws -> T {
        lock.lock()
        defer { lock.unlock() }

        guard let h = handle else {
            throw QpqError.notConnected
        }
        return try body(h)
    }

    private func checkStatus(_ code: Int32, handle: OpaquePointer) throws {
        guard code != QPQ_OK else { return }

        let msg: String
        if let errPtr = qpq_last_error(handle) {
            msg = String(cString: errPtr)
        } else {
            msg = "unknown error"
        }

        switch code {
        case QPQ_AUTH_FAILED:
            throw QpqError.authFailed(msg)
        case QPQ_TIMEOUT:
            throw QpqError.timeout(msg)
        case QPQ_NOT_CONNECTED:
            throw QpqError.notConnected
        default:
            throw QpqError.ffiError(msg)
        }
    }
}
