import Foundation

/// The initiator-side sync engine (the phone). Mirrors the host's `ConnectionHandler`: sends
/// the plaintext preamble, runs the Noise handshake (XXpsk0 to pair, IK to reconnect),
/// exchanges hello/welcome, then surfaces every `HostMsg` as an `Event` and accepts
/// `ClientMsg`s via `send`. Transport-agnostic over `FrameChannel`, so the iOS app wires it
/// to NWConnection and tests wire it to an in-memory pipe.
public actor SyncClient {
    public enum Mode: Sendable {
        case pair(secret: Data)   // XXpsk0, first-time pairing
        case reconnect            // IK against the pinned host static
    }

    public enum Event: Sendable {
        case connecting
        case ready(Welcome)
        case sessionList([SessionSummary])
        case snapshot(SessionSnapshot)
        case events(EventBatch)
        case approvalRequested(ApprovalRequest)
        case approvalResolved(ApprovalResolved)
        case sessionUpdated(SessionSummary)
        case dashboard(DashboardSnapshot)
        case wireError(WireError)
        case failed(String)
        case closed
    }

    private let channel: any FrameChannel
    private let identity: DeviceIdentity
    private let hostStaticKey: Data
    private let mode: Mode
    private let deviceID: String
    private let deviceLabel: String
    private let scopeClaim: DeviceScope
    private let pushToken: String?

    private var session: SecureSession?
    private var emit: AsyncStream<Event>.Continuation?

    // Inbound FIFO (same pattern as the host handler).
    private var pending: [Data] = []
    private var waiter: CheckedContinuation<Data?, Never>?
    private var inboundFinished = false

    public init(
        channel: any FrameChannel, identity: DeviceIdentity, hostStaticKey: Data,
        mode: Mode, deviceID: String, deviceLabel: String,
        scopeClaim: DeviceScope = .control, pushToken: String? = nil
    ) {
        self.channel = channel
        self.identity = identity
        self.hostStaticKey = hostStaticKey
        self.mode = mode
        self.deviceID = deviceID
        self.deviceLabel = deviceLabel
        self.scopeClaim = scopeClaim
        self.pushToken = pushToken
    }

    /// The session's static public key once established — pin this at pairing.
    public func hostKey() -> Data? { session?.remoteStaticKey }

    public func start() -> AsyncStream<Event> {
        AsyncStream { continuation in
            self.emit = continuation
            Task { await self.run() }
        }
    }

    public func send(_ msg: ClientMsg) {
        guard let session else { return }
        if let frame = try? session.seal(msg) { channel.send(frame) }
    }

    public func disconnect() { channel.close() }

    // MARK: - Run

    private func run() async {
        startPump()
        emit?.yield(.connecting)
        do {
            let secure = try await handshake()
            session = secure
            try await helloWelcome()
            try await messageLoop()
        } catch let wire as WireError {
            emit?.yield(.wireError(wire))
        } catch {
            emit?.yield(.failed("\(error)"))
        }
        emit?.yield(.closed)
        emit?.finish()
        channel.close()
    }

    private func handshake() async throws -> SecureSession {
        let psk: Data?
        let handshakeMode: HandshakeMode
        switch mode {
        case .pair(let secret): psk = secret; handshakeMode = .pair
        case .reconnect: psk = nil; handshakeMode = .reconnect
        }

        // Plaintext preamble so the host routes to the right pattern.
        let preamble = HandshakePreamble(mode: handshakeMode, deviceID: deviceID)
        channel.send(try CBOREncoder().encode(preamble))

        let driver = NoiseSession.makeInitiator(
            identity: identity, hostStaticKey: hostStaticKey, psk: psk)
        while !driver.isComplete {
            if driver.isMyTurnToWrite {
                channel.send(try driver.write())
            } else {
                guard let frame = await nextFrame() else { throw WireError(code: .unauthorized, message: "handshake closed") }
                _ = try driver.read(frame)
            }
        }
        return try driver.session()
    }

    private func helloWelcome() async throws {
        let hello = Hello(
            deviceID: deviceID, deviceLabel: deviceLabel,
            scopeClaim: scopeClaim, pushToken: pushToken)
        send(.hello(hello))
        guard let frame = await nextFrame() else { throw WireError(code: .unauthorized, message: "closed before welcome") }
        guard let session else { throw WireError(code: .malformed, message: "no session") }
        let msg = try session.open(HostMsg.self, from: frame)
        guard case .welcome(let welcome) = msg else {
            throw WireError(code: .malformed, message: "expected welcome")
        }
        emit?.yield(.ready(welcome))
    }

    private func messageLoop() async throws {
        while let frame = await nextFrame() {
            guard let session else { break }
            guard let msg = try? session.open(HostMsg.self, from: frame) else { continue }
            switch msg {
            case .welcome(let w): emit?.yield(.ready(w))
            case .sessionList(let l): emit?.yield(.sessionList(l))
            case .snapshot(let s): emit?.yield(.snapshot(s))
            case .events(let b): emit?.yield(.events(b))
            case .approvalRequested(let r): emit?.yield(.approvalRequested(r))
            case .approvalResolved(let r): emit?.yield(.approvalResolved(r))
            case .sessionUpdated(let s): emit?.yield(.sessionUpdated(s))
            case .dashboard(let d): emit?.yield(.dashboard(d))
            case .error(let e): emit?.yield(.wireError(e))
            case .pong, .unknown: break
            }
        }
    }

    // MARK: - Inbound FIFO

    private func feed(_ frame: Data) {
        if let w = waiter { waiter = nil; w.resume(returning: frame) }
        else { pending.append(frame) }
    }
    private func finishInbound() {
        inboundFinished = true
        if let w = waiter { waiter = nil; w.resume(returning: nil) }
    }
    private func nextFrame() async -> Data? {
        if !pending.isEmpty { return pending.removeFirst() }
        if inboundFinished { return nil }
        return await withCheckedContinuation { waiter = $0 }
    }
    private func startPump() {
        let frames = channel.frames()
        Task { [weak self] in
            for await frame in frames { await self?.feed(frame) }
            await self?.finishInbound()
        }
    }
}
