import Foundation
import Observation
import CoreGraphics
import RoyalVNCKit

@MainActor
@Observable
public final class SessionController {
    public private(set) var state: SessionState = .idle
    public private(set) var lastErrorMessage: String?
    public private(set) var framebufferSize: FramebufferSize?
    public private(set) var currentImage: CGImage?
    public private(set) var imageRevision: Int = 0
    public private(set) var lastUpdatedRegion: CGRect?
    public private(set) var cursorImage: CGImage?
    public private(set) var cursorHotspot: CGPoint = .zero
    public private(set) var screens: [RemoteScreen] = []
    public private(set) var desktopName: String?
    public private(set) var isReconnecting: Bool = false
    public private(set) var reconnectAttempt: Int = 0

    public var viewOnly: Bool
    public var quality: QualityPreset {
        didSet { applyQuality() }
    }
    public let clipboardSyncEnabled: Bool

    public let displayName: String
    public let host: String
    public let port: Int
    public let username: String

    private let keychainTag: String
    private let passwordProvider: any PasswordProviding
    private let preferredEncodings: [VNCFrameEncodingType]
    private let pathProvider: any NetworkPathProviding
    private let reconnectPolicy: ReconnectPolicy

    private var connection: VNCConnection?
    private var delegateAdapter: DelegateAdapter?
    private var redrawScheduled = false
    private var pendingDirtyRect: CGRect?
    private var reconnectTask: Task<Void, Never>?
    private var explicitlyStopped = false
    private var pathObserver: Task<Void, Never>?

    public init(
        displayName: String,
        host: String,
        port: Int,
        username: String = "",
        keychainTag: String,
        viewOnly: Bool = false,
        clipboardSyncEnabled: Bool = true,
        quality: QualityPreset = .adaptive,
        preferredEncodings: [VNCFrameEncodingType] = .default,
        passwordProvider: any PasswordProviding = DefaultPasswordProvider(),
        pathProvider: any NetworkPathProviding = NWPathObserver(),
        reconnectPolicy: ReconnectPolicy = .default
    ) {
        self.displayName = displayName
        self.host = host
        self.port = port
        self.username = username
        self.keychainTag = keychainTag
        self.viewOnly = viewOnly
        self.clipboardSyncEnabled = clipboardSyncEnabled
        self.quality = quality
        self.preferredEncodings = preferredEncodings
        self.passwordProvider = passwordProvider
        self.pathProvider = pathProvider
        self.reconnectPolicy = reconnectPolicy
    }

    public convenience init(
        connection saved: SavedConnection,
        passwordProvider: any PasswordProviding = DefaultPasswordProvider()
    ) {
        let decoded = [VNCFrameEncodingType].decode(saved.preferredEncodings)
        self.init(
            displayName: saved.displayName,
            host: saved.host,
            port: saved.port,
            username: saved.username,
            keychainTag: saved.keychainTag,
            viewOnly: saved.viewOnly,
            clipboardSyncEnabled: saved.clipboardSyncEnabled,
            quality: saved.quality,
            preferredEncodings: decoded.isEmpty ? .default : decoded,
            passwordProvider: passwordProvider
        )
    }

    public func start() {
        explicitlyStopped = false
        reconnectAttempt = 0
        beginConnect()
        startObservingPath()
    }

    public func stop() {
        explicitlyStopped = true
        reconnectTask?.cancel()
        reconnectTask = nil
        pathObserver?.cancel()
        pathObserver = nil
        connection?.disconnect()
    }

    public func reconnectNow() {
        reconnectTask?.cancel()
        reconnectTask = nil
        connection?.disconnect()
        beginConnect()
    }

    // MARK: Pointer

    public func pointerMove(toNormalized point: CGPoint) {
        guard let pos = framebufferPoint(for: point) else { return }
        connection?.mouseMove(x: pos.x, y: pos.y)
    }

    public func pointerDown(_ button: PointerButton, atNormalized point: CGPoint) {
        guard !viewOnly, let pos = framebufferPoint(for: point) else { return }
        connection?.mouseButtonDown(button.vnc, x: pos.x, y: pos.y)
    }

    public func pointerUp(_ button: PointerButton, atNormalized point: CGPoint) {
        guard !viewOnly, let pos = framebufferPoint(for: point) else { return }
        connection?.mouseButtonUp(button.vnc, x: pos.x, y: pos.y)
    }

    public func pointerClick(_ button: PointerButton, atNormalized point: CGPoint) {
        guard !viewOnly, let pos = framebufferPoint(for: point) else { return }
        connection?.mouseMove(x: pos.x, y: pos.y)
        connection?.mouseButtonDown(button.vnc, x: pos.x, y: pos.y)
        connection?.mouseButtonUp(button.vnc, x: pos.x, y: pos.y)
    }

    public func pointerScroll(_ direction: ScrollDirection,
                              steps: UInt32,
                              atNormalized point: CGPoint) {
        guard !viewOnly, steps > 0, let pos = framebufferPoint(for: point) else { return }
        connection?.mouseWheel(direction.vnc, x: pos.x, y: pos.y, steps: steps)
    }

    // MARK: Keyboard

    public func keyDown(keysym: UInt32) {
        guard !viewOnly else { return }
        connection?.keyDown(VNCKeyCode(keysym))
    }

    public func keyUp(keysym: UInt32) {
        guard !viewOnly else { return }
        connection?.keyUp(VNCKeyCode(keysym))
    }

    public func type(_ string: String) {
        guard !viewOnly else { return }
        let events = Self.keyEvents(for: string)
        recordTypedEvents(events)
        guard let connection else { return }
        for event in events {
            let code = VNCKeyCode(event.keysym)
            if event.isDown {
                connection.keyDown(code)
            } else {
                connection.keyUp(code)
            }
        }
    }

    /// Pure mapping from a string to the key down/up keysym pairs it should
    /// generate. Newlines map to X11 Return; all other printable ASCII maps
    /// 1:1 to the equivalent X11 keysym (which equals the ASCII code).
    public nonisolated static func keyEvents(for string: String) -> [KeyEvent] {
        var events: [KeyEvent] = []
        for char in string {
            if char.isNewline {
                events.append(KeyEvent(keysym: 0xFF0D, isDown: true))
                events.append(KeyEvent(keysym: 0xFF0D, isDown: false))
                continue
            }
            for code in VNCKeyCode.withCharacter(char) {
                events.append(KeyEvent(keysym: code.rawValue, isDown: true))
                events.append(KeyEvent(keysym: code.rawValue, isDown: false))
            }
        }
        return events
    }

    public struct KeyEvent: Equatable, Sendable {
        public let keysym: UInt32
        public let isDown: Bool
    }

    /// Test hook: when set, every typed event is appended here in addition to
    /// being sent over the wire. Used by UI tests to verify keyboard plumbing.
    public var typedEventLog: [KeyEvent] = []
    public var typedEventLogEnabled: Bool = false
    private func recordTypedEvents(_ events: [KeyEvent]) {
        guard typedEventLogEnabled else { return }
        typedEventLog.append(contentsOf: events)
    }

    public func sendBackspace() { pressKey(.delete) }
    public func sendReturn() { pressKey(.return) }
    public func sendEscape() { pressKey(.escape) }
    public func sendTab() { pressKey(.tab) }
    public func sendArrow(_ direction: ScrollDirection) {
        switch direction {
        case .up: pressKey(.upArrow)
        case .down: pressKey(.downArrow)
        case .left: pressKey(.leftArrow)
        case .right: pressKey(.rightArrow)
        }
    }
    public func sendFunctionKey(_ index: Int) {
        guard let code = VNCKeyCode.functionKey(index) else { return }
        pressKey(code)
    }

    public func pressKeyCombo(_ keys: [VNCKeyCode]) {
        guard !viewOnly, let connection else { return }
        for code in keys { connection.keyDown(code) }
        for code in keys.reversed() { connection.keyUp(code) }
    }

    private func pressKey(_ code: VNCKeyCode) {
        guard !viewOnly else { return }
        connection?.keyDown(code)
        connection?.keyUp(code)
    }

    // MARK: Clipboard

    public func pushLocalClipboardNow(_ text: String) {
        ClipboardSink.set(text)
    }

    // MARK: Internal handlers (invoked on MainActor by adapter)

    func handleConnectionState(status: VNCConnection.Status, errorMessage: String?) {
        switch status {
        case .connecting:
            state = .connecting
        case .connected:
            isReconnecting = false
            reconnectAttempt = 0
            if let size = framebufferSize {
                state = .connected(framebufferSize: size)
            } else {
                state = .authenticating
            }
        case .disconnecting:
            break
        case .disconnected:
            let reason: DisconnectReason
            if let errorMessage {
                if errorMessage.lowercased().contains("authentication") {
                    reason = .authenticationFailed
                } else {
                    reason = .networkError(errorMessage)
                }
                lastErrorMessage = errorMessage
            } else {
                reason = explicitlyStopped ? .userRequested : .remoteClosed
            }
            state = .disconnected(reason: reason)
            connection?.delegate = nil
            connection = nil
            delegateAdapter = nil
            if !explicitlyStopped, reconnectPolicy.shouldReconnect(for: reason) {
                scheduleReconnect()
            }
        }
    }

    func handleFramebufferCreated(size: FramebufferSize, screens: [RemoteScreen]) {
        framebufferSize = size
        self.screens = screens
        state = .connected(framebufferSize: size)
        applyQuality()
    }

    func handleFramebufferResized(size: FramebufferSize, screens: [RemoteScreen]) {
        framebufferSize = size
        self.screens = screens
        state = .connected(framebufferSize: size)
    }

    func handleFramebufferUpdated(region: CGRect) {
        if let existing = pendingDirtyRect {
            pendingDirtyRect = existing.union(region)
        } else {
            pendingDirtyRect = region
        }
        guard !redrawScheduled else { return }
        redrawScheduled = true
        Task { @MainActor in
            self.redrawScheduled = false
            self.refreshImage()
        }
    }

    func handleCursorUpdate(image: CGImage?, hotspot: CGPoint) {
        cursorImage = image
        cursorHotspot = hotspot
    }

    func refreshImage() {
        guard let connection, let fb = connection.framebuffer else { return }
        currentImage = fb.cgImage
        lastUpdatedRegion = pendingDirtyRect
        pendingDirtyRect = nil
        imageRevision &+= 1
    }

    // MARK: Helpers

    private func beginConnect() {
        if connection != nil { return }
        state = .connecting
        lastErrorMessage = nil
        let settings = VNCConnection.Settings(
            isDebugLoggingEnabled: false,
            hostname: host,
            port: UInt16(clamping: port),
            isShared: true,
            isScalingEnabled: false,
            useDisplayLink: false,
            // Any value other than .none, otherwise RoyalVNCKit drops every
            // enqueued PointerEvent/KeyEvent. On iOS we don't have local
            // keyboard shortcuts to worry about, so pick the most permissive
            // forwarding mode.
            inputMode: .forwardKeyboardShortcutsEvenIfInUseLocally,
            isClipboardRedirectionEnabled: clipboardSyncEnabled,
            colorDepth: .depth24Bit,
            frameEncodings: preferredEncodings
        )
        let connection = VNCConnection(settings: settings)
        let adapter = DelegateAdapter(
            controller: self,
            passwordProvider: passwordProvider,
            keychainTag: keychainTag,
            username: username
        )
        connection.delegate = adapter
        self.connection = connection
        self.delegateAdapter = adapter
        connection.connect()
    }

    private func scheduleReconnect() {
        let attempt = reconnectAttempt + 1
        reconnectAttempt = attempt
        guard let delay = reconnectPolicy.delay(for: attempt) else {
            isReconnecting = false
            return
        }
        isReconnecting = true
        reconnectTask?.cancel()
        reconnectTask = Task { @MainActor [weak self] in
            try? await Task.sleep(for: .milliseconds(Int(delay * 1000)))
            guard let self, !self.explicitlyStopped else { return }
            self.beginConnect()
        }
    }

    private func startObservingPath() {
        pathObserver?.cancel()
        let stream = pathProvider.pathChanges
        pathObserver = Task { @MainActor [weak self] in
            for await change in stream {
                guard let self else { return }
                if change.isExpensive != self.lastPathExpensive {
                    self.lastPathExpensive = change.isExpensive
                    self.applyQuality()
                }
            }
        }
    }

    private var lastPathExpensive: Bool = false

    private func applyQuality() {
        // Phase 2 hook: with extended encodings we'd push CompressionLevel/JPEGQuality
        // pseudo-encodings here. RoyalVNCKit currently picks fixed levels internally;
        // the connection re-orders frame encodings on next handshake. For now this is
        // a no-op stub; the field is wired so QualityPreset round-trips through state.
        _ = quality
    }

    private func framebufferPoint(for normalized: CGPoint) -> (x: UInt16, y: UInt16)? {
        guard let size = framebufferSize else { return nil }
        let nx = max(0, min(1, normalized.x))
        let ny = max(0, min(1, normalized.y))
        let x = UInt16(clamping: Int((nx * CGFloat(size.width - 1)).rounded()))
        let y = UInt16(clamping: Int((ny * CGFloat(size.height - 1)).rounded()))
        return (x, y)
    }
}

// MARK: - Delegate Adapter

private final class DelegateAdapter: NSObject, VNCConnectionDelegate, @unchecked Sendable {
    weak var controller: SessionController?
    let passwordProvider: any PasswordProviding
    let keychainTag: String
    let username: String

    init(controller: SessionController,
         passwordProvider: any PasswordProviding,
         keychainTag: String,
         username: String) {
        self.controller = controller
        self.passwordProvider = passwordProvider
        self.keychainTag = keychainTag
        self.username = username
    }

    func connection(_ connection: VNCConnection,
                    stateDidChange newState: VNCConnection.ConnectionState) {
        let status = newState.status
        let errorMessage: String? = newState.error.flatMap { error in
            if let localized = error as? LocalizedError, let desc = localized.errorDescription {
                return desc
            }
            return String(describing: error)
        }
        Task { @MainActor in
            self.controller?.handleConnectionState(status: status, errorMessage: errorMessage)
        }
    }

    func connection(_ connection: VNCConnection,
                    credentialFor authenticationType: VNCAuthenticationType,
                    completion: @escaping (VNCCredential?) -> Void) {
        let pwd = passwordProvider.password(for: keychainTag) ?? ""
        let credential: VNCCredential
        switch authenticationType {
        case .vnc:
            credential = VNCPasswordCredential(password: pwd)
        case .appleRemoteDesktop, .ultraVNCMSLogonII:
            credential = VNCUsernamePasswordCredential(username: username, password: pwd)
        @unknown default:
            credential = VNCPasswordCredential(password: pwd)
        }
        completion(credential)
    }

    func connection(_ connection: VNCConnection,
                    didCreateFramebuffer framebuffer: VNCFramebuffer) {
        let size = FramebufferSize(width: Int(framebuffer.size.width),
                                   height: Int(framebuffer.size.height))
        let screens = framebuffer.screens.map {
            RemoteScreen(id: $0.id, frame: $0.cgFrame)
        }
        Task { @MainActor in
            self.controller?.handleFramebufferCreated(size: size, screens: screens)
            self.controller?.refreshImage()
        }
    }

    func connection(_ connection: VNCConnection,
                    didResizeFramebuffer framebuffer: VNCFramebuffer) {
        let size = FramebufferSize(width: Int(framebuffer.size.width),
                                   height: Int(framebuffer.size.height))
        let screens = framebuffer.screens.map {
            RemoteScreen(id: $0.id, frame: $0.cgFrame)
        }
        Task { @MainActor in
            self.controller?.handleFramebufferResized(size: size, screens: screens)
            self.controller?.refreshImage()
        }
    }

    func connection(_ connection: VNCConnection,
                    didUpdateFramebuffer framebuffer: VNCFramebuffer,
                    x: UInt16, y: UInt16,
                    width: UInt16, height: UInt16) {
        let region = CGRect(x: Int(x), y: Int(y), width: Int(width), height: Int(height))
        Task { @MainActor in
            self.controller?.handleFramebufferUpdated(region: region)
        }
    }

    func connection(_ connection: VNCConnection,
                    didUpdateCursor cursor: VNCCursor) {
        let imageBox = SendableValueBox(cursor.cgImage)
        let hotspot = cursor.cgHotspot
        Task { @MainActor in
            self.controller?.handleCursorUpdate(image: imageBox.value, hotspot: hotspot)
        }
    }
}

private extension VNCKeyCode {
    static func functionKey(_ index: Int) -> VNCKeyCode? {
        switch index {
        case 1: .f1
        case 2: .f2
        case 3: .f3
        case 4: .f4
        case 5: .f5
        case 6: .f6
        case 7: .f7
        case 8: .f8
        case 9: .f9
        case 10: .f10
        case 11: .f11
        case 12: .f12
        case 13: .f13
        case 14: .f14
        case 15: .f15
        case 16: .f16
        case 17: .f17
        case 18: .f18
        case 19: .f19
        default: nil
        }
    }
}

private extension PointerButton {
    var vnc: VNCMouseButton {
        switch self {
        case .left: .left
        case .middle: .middle
        case .right: .right
        }
    }
}

private extension ScrollDirection {
    var vnc: VNCMouseWheel {
        switch self {
        case .up: .up
        case .down: .down
        case .left: .left
        case .right: .right
        }
    }
}
