import Foundation
import SwiftUI
import SwiftData

final class RequestCoordinator: ObservableObject {
    
    @Published
    var serverChallenge: ServerChallenge? = nil
    
    @Published
    var state: MessageResult = .noKeyAvailable
    
    @Published
    private var timer: Timer?
    
    @Published
    var pendingRequests: [PendingOperation] = []
    
    @Published
    var activeRequest: PendingOperation?
    
    @Published
    var keyManager = KeyManagement()
    
    @AppStorage("server")
    var serverPath: String = "https://christophhagen.de/sesame/"
    
    @AppStorage("localIP")
    var localAddress: String = "192.168.178.104/"
    
    @AppStorage("connectionType")
    var connectionType: ConnectionStrategy = .remoteFirst
    
    let modelContext: ModelContext
    
    init(modelContext: ModelContext) {
        self.modelContext = modelContext
        if keyManager.hasAllKeys {
            self.state = .notChecked
        }
    }
    
    let client = Client()
    
    var needsNewServerChallenge: Bool {
        serverChallenge?.isExpired ?? true
    }
    
    @Published
    var isPerformingRequest: Bool = false
    
    func startUnlock() {
        addOperations(.challenge, .unlock)
    }
    
    func startChallenge() {
        addOperations(.challenge)
    }
    
    private func addOperations(_ operations: RequestType...) {
        #warning("Only perform challenge when doing unlock? Remove code complexity")
        // Just add all operations for an unlock
        // For every completed operation, the unnecessary ones will be removed without executing them
        let operations = connectionType.transmissionTypes.map { route in
            operations.map { PendingOperation(route: route, operation: $0) }
        }.joined()
        
        pendingRequests.append(contentsOf: operations)
        continueRequests()
    }
    
    private func continueRequests() {
        guard activeRequest == nil else {
            return
        }
        guard !pendingRequests.isEmpty else {
            self.isPerformingRequest = false
            return
        }
        let activeRequest = pendingRequests.removeFirst()
        self.activeRequest = activeRequest
        self.isPerformingRequest = true
        Task {
            await process(request: activeRequest)
        }
    }
    
    private func process(request: PendingOperation) async {
        let startTime = Date.now
        let (success, response, challenge) = await self.start(request)
        let endTime = Date.now
        let roundTripTime = endTime.timeIntervalSince(startTime)
        
        if let s = challenge?.message {
            print("\(s) took \(Int(roundTripTime * 1000)) ms")
        } else {
            print("\(request.operation.description) took \(Int(roundTripTime * 1000)) ms")
        }
        
        if request.operation == .unlock, let response {
            print("Saving history item")
            let item = HistoryItem(message: response, startDate: startTime, route: request.route, finishDate: endTime)
            modelContext.insert(item)
        }

        DispatchQueue.main.async {
            self.filterPendingRequests(after: request, success: success, hasChallenge: challenge != nil)
            if let response {
                self.state = response.result
            }
            if let challenge {
                self.serverChallenge = challenge
            }
            self.activeRequest = nil
            self.continueRequests()
        }
    }
    
    private func filterPendingRequests(after operation: PendingOperation, success: Bool, hasChallenge: Bool) {
        if success {
            // Filter all unlocks
            if operation.operation == .unlock {
                // Successful unlock means no need for next challenge or unlocks, so remove all
                self.pendingRequests = []
            } else {
                // Successful challenge means no need for additional challenges, but keep unlocks
                self.pendingRequests = pendingRequests.filter { $0.operation != .challenge }
            }
        } else {
            // Filter all operations with the same route for connection errors
            // And with type, depending on error?
        }
    }
    
    private func start(_ operation: PendingOperation) async -> OptionalServerResponse {
        switch operation.operation {
        case .challenge:
            if let serverChallenge, !serverChallenge.isExpired {
                return (true, serverChallenge.message, serverChallenge)
            }
            return await performChallenge(route: operation.route)
            
        case .unlock:
            guard let serverChallenge, !serverChallenge.isExpired else {
                return (false, nil, nil)
            }
            return await performUnlock(with: serverChallenge.message, route: operation.route)
        }
    }
    
    private func performChallenge(route: TransmissionType) async -> OptionalServerResponse {
        let initialMessage = Message.initial()
        let (result, challenge) = await send(initialMessage, route: route)
        guard let message = challenge?.message else {
            return (false, result, nil)
        }
        // Can't get here without the message being accepted
        guard message.messageType == .challenge else {
            print("Invalid message type for challenge: \(message)")
            return (false, result.with(result: .invalidMessageTypeFromDevice), nil)
        }
        return (true, result.with(result: .deviceAvailable), challenge)
    }
    
    private func performUnlock(with challenge: Message, route: TransmissionType) async -> OptionalServerResponse {
        let request = challenge.requestMessage()
        let (unlockState, responseData) = await send(request, route: route)
        
        guard let response = responseData?.message else {
            return (false, unlockState, nil)
        }
        switch response.messageType {
        case .initial, .request:
            print("Invalid message type for response: \(response)")
            return (false, response.with(result: .invalidMessageTypeFromDevice), nil)
        case .challenge:
            // New challenge received, challenge was expired
            return (true, unlockState, responseData)
        case .response:
            break
        }
        
        guard response.serverChallenge == request.serverChallenge else {
            print("Invalid server challenge for unlock: \(response)")
            return (false, response.with(result: .invalidServerChallengeFromDevice), nil)
        }
        return (true, response.with(result: .unlocked), nil)
    }
    
    private func url(for route: TransmissionType) -> String {
        switch route {
        case .throughServer:
            return serverPath
        case .overLocalWifi:
            return localAddress
        }
    }
    
    private func send(_ message: Message, route: TransmissionType) async -> ServerResponse {
        guard let keys = keyManager.getAllKeys() else {
            return (message.with(result: .noKeyAvailable), nil)
        }
        let url = url(for: route)
        return await client.send(message, to: url, through: route, using: keys)
    }

    func startUpdatingServerChallenge() {
        guard timer == nil else {
            return
        }
        DispatchQueue.main.async {
            self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] timer in
                guard let self else {
                    timer.invalidate()
                    return
                }
                DispatchQueue.main.async {
                    self.startChallenge()
                }
            }
            self.timer!.fire()
        }
    }

    func endUpdatingServerChallenge() {
        timer?.invalidate()
        timer = nil
    }
}
