//
//  ConnectionManager.swift
//  SwanDesk — app-side VPN driver and source of truth for the UI.
//
//  Owns the current runtime status (pushed from the daemon over reverse XPC),
//  the live log buffer, and connect/disconnect actions. SwiftUI views observe it.
//

import SwiftUI

@MainActor
@Observable
final class ConnectionManager {
    static let shared = ConnectionManager()

    /// Latest status reported by the daemon.
    private(set) var status = VPNRuntimeStatus()
    /// Rolling log buffer for the log pane (capped).
    private(set) var logLines: [String] = []
    /// True while a connect/disconnect XPC call is in flight.
    private(set) var busy = false

    private let helper = Helper.shared
    private let maxLogLines = 2000

    private init() {
        // Wire reverse-XPC callbacks from the daemon to the main actor. Use the
        // main queue (FIFO) rather than `Task { @MainActor }` so rapid frames
        // (e.g. connecting → connected) are applied IN ORDER — unstructured Tasks
        // have no ordering guarantee and could leave the UI on a stale state.
        helper.client.onStatus = { [weak self] status in
            DispatchQueue.main.async { MainActor.assumeIsolated { self?.status = status } }
        }
        helper.client.onLog = { [weak self] line in
            DispatchQueue.main.async { MainActor.assumeIsolated { self?.append(log: line) } }
        }
    }

    /// Bring up the daemon, confirm we can reach it, then subscribe to its push
    /// events and pull the current status. Returns `false` if the helper could not
    /// be reached even after recovery — the caller refuses to run in that case.
    @discardableResult
    func bootstrap() async -> Bool {
        guard await helper.establishConnection() else { return false }
        helper.subscribe()
        self.status = await helper.fetchStatus()
        return true
    }

    private func append(log line: String) {
        logLines.append(line)
        if logLines.count > maxLogLines {
            logLines.removeFirst(logLines.count - maxLogLines)
        }
    }

    func clearLog() { logLines.removeAll() }

    // MARK: Actions

    func connect(profile: VPNProfile, secrets: VPNSecrets) {
        guard !busy else { return }
        busy = true
        Task {
            let (code, message) = await helper.start(profile: profile, secrets: secrets)
            self.busy = false
            if code != 0 { self.append(log: "Connect failed: \(message)") }
        }
    }

    func disconnect() {
        guard !busy else { return }
        busy = true
        Task {
            let (_, message) = await helper.stop()
            self.busy = false
            self.append(log: message)
        }
    }

    /// Disconnect and wait for teardown to finish. Used on app quit so the tunnel,
    /// routes and DNS are fully restored before the process exits.
    func disconnectAndWait() async {
        busy = true
        let (_, message) = await helper.stop()
        busy = false
        append(log: message)
    }

    /// Disconnect on quit, but never block termination longer than `timeout` — if
    /// the daemon is wedged we'd rather quit than hang the app forever.
    func disconnectForQuit(timeout: TimeInterval = 6) async {
        await withTaskGroup(of: Void.self) { group in
            group.addTask { await self.disconnectAndWait() }
            group.addTask { try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) }
            await group.next()      // whichever finishes first
            group.cancelAll()
        }
    }

    /// True when there is something to tear down (anything but cleanly disconnected).
    var hasActiveTunnel: Bool { status.state != .disconnected }

    // MARK: Derived UI state

    var isConnected: Bool { status.state == .connected }

    /// SF Symbol for the menu-bar item, conveying state by shape as well as colour
    /// (menu-bar template rendering can mute colour).
    var menuBarSymbol: String {
        switch status.state {
        case .connected:                 return "lock.fill"
        case .connecting, .reconnecting: return "lock.rotation"
        case .disconnected, .error:      return "lock.open.fill"
        }
    }

    var stateColor: Color {
        switch status.state {
        case .connected:    return .green
        case .connecting, .reconnecting: return .orange
        case .disconnected: return .red
        case .error:        return .red
        }
    }
    var stateText: String {
        switch status.state {
        case .connected:    return "Connected" + (status.assignedVIP.map { " (\($0))" } ?? "")
        case .connecting:   return "Connecting…"
        case .reconnecting: return "Reconnecting…"
        case .disconnected: return "Disconnected"
        case .error:        return status.message ?? "Error"
        }
    }
}
