import Foundation
import NucleicProtocol

/// Drives one client connection: routes + runs the Noise handshake, authenticates the
/// device, exchanges hello/welcome, then services `ClientMsg`s and pushes filtered
/// `HostBroadcast`s. One actor per connection — all sealing/sending is actor-isolated, so
/// the Noise nonce order matches the wire order.
actor ConnectionHandler {
    let id = UUID()
    private let connection: any FrameChannel
    private let bridge: any SyncHostBridge
    private let identity: DeviceIdentity
    private let store: any PairedDeviceStore
    private weak var host: SyncHost?

    private var session: SecureSession?
    private var deviceID = ""
    private var grantedScope: DeviceScope = .approve
    private var subscriptions: [SessionID: Verbosity] = [:]
    private var closed = false

    // A minimal single-consumer async FIFO. A detached pump task owns the (non-Sendable)
    // stream iterator and feeds frames in; `nextFrame()` is the actor-isolated consumer.
    // This keeps iteration out of the actor's isolation region (which Swift 6 forbids for a
    // stored mutating iterator) while serializing consumption.
    private var pending: [Data] = []
    private var waiter: CheckedContinuation<Data?, Never>?
    private var inboundFinished = false

    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 { continuation in waiter = continuation }
    }
    private func startPump() {
        let frames = connection.frames()
        Task { [weak self] in
            for await frame in frames { await self?.feed(frame) }
            await self?.finishInbound()
        }
    }

    init(
        connection: any FrameChannel, bridge: any SyncHostBridge,
        identity: DeviceIdentity, store: any PairedDeviceStore, host: SyncHost
    ) {
        self.connection = connection
        self.bridge = bridge
        self.identity = identity
        self.store = store
        self.host = host
    }

    var peerDescription: String { connection.peerDescription }

    // MARK: - Lifecycle

    func run() async {
        startPump()
        do {
            let secure = try await handshake()
            self.session = secure
            try await helloWelcome()
            await host?.register(self)
            try await messageLoop()
        } catch let error as WireError {
            sendIfPossible(.error(error))
        } catch {
            // Transport/handshake failure: nothing more we can say securely.
        }
        await close()
    }

    func close() async {
        guard !closed else { return }
        closed = true
        await host?.unregister(self)
        connection.close()
    }

    // MARK: - Handshake routing (SYNC §4.2/§4.3)

    private func handshake() async throws -> SecureSession {
        guard let preFrame = await nextFrame() else { throw SyncTransportError.connectionClosed }
        let preamble = try CBORDecoder().decode(HandshakePreamble.self, from: preFrame)
        guard preamble.protocolVersion == SyncProtocol.version else {
            throw WireError(code: .protocolVersion, message: "handshake protocol mismatch")
        }
        deviceID = preamble.deviceID

        let driver: HandshakeDriver
        var expectedStatic: Data?
        switch preamble.mode {
        case .pair:
            guard let psk = await host?.activePairingSecret() else {
                throw WireError(code: .unauthorized, message: "no pairing in progress")
            }
            driver = NoiseSession.makeResponder(identity: identity, psk: psk)
        case .reconnect:
            guard let device = await store.device(preamble.deviceID) else {
                throw WireError(code: .unauthorized, message: "unknown device")
            }
            expectedStatic = device.staticPublicKey
            grantedScope = device.scope
            driver = NoiseSession.makeResponder(identity: identity, psk: nil)
        }

        while !driver.isComplete {
            if driver.isMyTurnToWrite {
                connection.send(try driver.write())
            } else {
                guard let frame = await nextFrame() else { throw SyncTransportError.connectionClosed }
                _ = try driver.read(frame)
            }
        }
        let secure = try driver.session()

        // Reconnect: the recovered peer static must match the pin (revocation gate).
        if preamble.mode == .reconnect, secure.remoteStaticKey != expectedStatic {
            throw WireError(code: .unauthorized, message: "device key mismatch")
        }
        return secure
    }

    // MARK: - hello / welcome

    private func helloWelcome() async throws {
        guard let frame = await nextFrame() else { throw SyncTransportError.connectionClosed }
        let msg = try open(frame)
        guard case .hello(let hello) = msg else {
            throw WireError(code: .malformed, message: "expected hello")
        }
        guard hello.protocolVersion == SyncProtocol.version else {
            throw WireError(code: .protocolVersion, message: "protocol mismatch")
        }
        let now = Date()
        if let existing = await store.device(deviceID) {
            // Reconnect: refresh metadata, keep the pin + granted scope.
            var updated = existing
            updated.label = hello.deviceLabel
            updated.pushToken = hello.pushToken ?? existing.pushToken
            updated.lastSeenAt = now
            await store.upsert(updated)
            grantedScope = existing.scope
        } else {
            // First pairing (TOFU): pin the peer static we just authenticated. The phone is
            // a full control-scope client (parity with the Mac), so grant up to control.
            grantedScope = min(hello.scopeClaim, .control)
            await store.upsert(PairedDevice(
                deviceID: deviceID, label: hello.deviceLabel,
                staticPublicKey: session!.remoteStaticKey, scope: grantedScope,
                pushToken: hello.pushToken, pairedAt: now, lastSeenAt: now))
            await host?.didPair()  // pairing consumed; host can drop the QR
        }

        let welcome = Welcome(
            grantedScope: grantedScope,
            host: await bridge.hostInfo,
            capabilities: await bridge.capabilities,
            modelCatalog: await bridge.modelCatalog)
        send(.welcome(welcome))
    }

    // MARK: - Message loop

    private func messageLoop() async throws {
        while let frame = await nextFrame() {
            let msg: ClientMsg
            do { msg = try open(frame) }
            catch { send(.error(WireError(code: .malformed, message: "undecodable frame"))); continue }
            await dispatch(msg)
        }
    }

    private func dispatch(_ msg: ClientMsg) async {
        switch msg {
        case .hello:
            break  // already handled; ignore duplicates
        case .ping:
            send(.pong)
        case .listSessions:
            send(.sessionList(await bridge.sessionSummaries()))
        case .subscribe(let sub):
            subscriptions[sub.sessionID] = sub.verbosity
            if let snapshot = await bridge.snapshot(sub.sessionID, sinceSeq: sub.sinceSeq) {
                send(.snapshot(snapshot))
            } else {
                send(.error(WireError(code: .unknownSession, message: "no such session", sessionID: sub.sessionID)))
            }
        case .unsubscribe(let id):
            subscriptions[id] = nil
        case .approvalRespond(let id, let decision):
            guard grantedScope >= .approve else {
                send(.error(WireError(code: .unauthorized, message: "scope too low"))); return
            }
            await guardedCall { try await self.bridge.respondToApproval(id, decision) }
        case .sendInput(let id, let input):
            guard grantedScope >= .approve else {
                send(.error(WireError(code: .unauthorized, message: "scope too low", sessionID: id))); return
            }
            await guardedCall(sessionID: id) { try await self.bridge.sendInput(id, input) }
        case .interrupt(let id):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.interrupt(id) }
        case .listDashboard:
            send(.dashboard(await bridge.dashboardSnapshot()))
        case .startChat(let request):
            guard requireControl() else { return }
            await guardedCall { try await self.bridge.startChat(request) }
        case .captureTodo(let request):
            guard requireControl() else { return }
            await guardedCall { try await self.bridge.captureTodo(request) }
        case .dispatchTodo(let todo, let project):
            guard requireControl() else { return }
            await guardedCall { try await self.bridge.dispatchTodo(todo, in: project) }
        case .setTodoStatus(let todo, let status):
            guard requireControl() else { return }
            await guardedCall { try await self.bridge.remoteSetTodoStatus(todo, status) }
        case .deleteTodo(let todo):
            guard requireControl() else { return }
            await guardedCall { try await self.bridge.remoteDeleteTodo(todo) }
        case .renameSession(let id, let title):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.renameSession(id, to: title) }
        case .setFavorite(let id, let flag):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setFavorite(id, flag) }
        case .setArchived(let id, let flag):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setArchived(id, flag) }
        case .deleteSession(let id):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.deleteSessionRemote(id) }
        case .integrate(let id, let mode):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.integrate(id, mode) }
        case .discard(let id):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.discard(id) }
        case .setSessionModel(let id, let model):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setSessionModel(id, model) }
        case .setSessionEffort(let id, let effort):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setSessionEffort(id, effort) }
        case .setSessionAuto(let id, let flag):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setSessionAuto(id, flag) }
        case .setSessionAutoShip(let id, let flag):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setSessionAutoShip(id, flag) }
        case .setSessionShipBranch(let id, let branch):
            guard requireControl(id) else { return }
            await guardedCall(sessionID: id) { try await self.bridge.setSessionShipBranch(id, branch) }
        }
    }

    /// Gate a control-scope action; emits an `unauthorized` error and returns false if denied.
    private func requireControl(_ sessionID: SessionID? = nil) -> Bool {
        guard grantedScope >= .control else {
            send(.error(WireError(code: .unauthorized, message: "control scope required", sessionID: sessionID)))
            return false
        }
        return true
    }

    private func guardedCall(sessionID: SessionID? = nil, _ body: () async throws -> Void) async {
        do { try await body() }
        catch let wire as WireError { send(.error(wire)) }
        catch { send(.error(WireError(code: .backpressure, message: "\(error)", sessionID: sessionID))) }
    }

    // MARK: - Broadcast fan-out (per-subscription + verbosity filtering, SYNC §5.5)

    func deliver(_ broadcast: HostBroadcast) {
        guard session != nil else { return }
        switch broadcast {
        case .events(let sid, let events):
            guard let verbosity = subscriptions[sid], verbosity != .statusOnly else {
                // statusOnly clients still want terminal runFinished for the session.
                if subscriptions[sid] == .statusOnly {
                    let terminal = events.filter { if case .runFinished = $0.kind { return true } else { return false } }
                    if !terminal.isEmpty { send(.events(EventBatch(sessionID: sid, events: terminal))) }
                }
                return
            }
            let filtered = verbosity == .full ? events : events.filter { !isPartialDelta($0) }
            if !filtered.isEmpty { send(.events(EventBatch(sessionID: sid, events: filtered))) }
        case .approvalRequested(let req):
            if subscriptions[req.sessionID] != nil { send(.approvalRequested(req)) }
        case .approvalResolved(let resolved):
            send(.approvalResolved(resolved))  // harmless to all; clients dismiss by id
        case .sessionUpdated(let summary):
            send(.sessionUpdated(summary))
        case .dashboard(let snapshot):
            send(.dashboard(snapshot))  // Home/Projects/To-Dos refresh
        }
    }

    private func isPartialDelta(_ event: AgentEvent) -> Bool {
        switch event.kind {
        case .toolCallInputDelta: return true
        case .assistantText(let c), .thinking(let c): return c.isPartial
        default: return false
        }
    }

    // MARK: - Sealing

    private func send(_ msg: HostMsg) {
        guard let session else { return }
        if let frame = try? session.seal(msg) { connection.send(frame) }
    }
    private func sendIfPossible(_ msg: HostMsg) { send(msg) }

    private func open(_ frame: Data) throws -> ClientMsg {
        guard let session else { throw WireError(code: .malformed, message: "no session") }
        return try session.open(ClientMsg.self, from: frame)
    }
}

extension ConnectionHandler: Equatable, Hashable {
    nonisolated static func == (lhs: ConnectionHandler, rhs: ConnectionHandler) -> Bool { lhs.id == rhs.id }
    nonisolated func hash(into hasher: inout Hasher) { hasher.combine(id) }
}
