//
//  ConnectionManager.swift
//  AltServer
//
//  Created by Riley Testut on 5/23/19.
//  Copyright © 2019 Riley Testut. All rights reserved.
//

import Foundation
import Network
import AppKit

import AltKit

extension ALTServerError
{
    init<E: Error>(_ error: E)
    {
        switch error
        {
        case let error as ALTServerError: self = error
        case is DecodingError: self = ALTServerError(.invalidRequest)
        case is EncodingError: self = ALTServerError(.invalidResponse)
        default:
            assertionFailure("Caught unknown error type")
            self = ALTServerError(.unknown)
        }
    }
}

extension ConnectionManager
{
    enum State
    {
        case notRunning
        case connecting
        case running(NWListener.Service)
        case failed(Swift.Error)
    }
}

class ConnectionManager
{
    static let shared = ConnectionManager()
    
    var stateUpdateHandler: ((State) -> Void)?
    
    private(set) var state: State = .notRunning {
        didSet {
            self.stateUpdateHandler?(self.state)
        }
    }
    
    private lazy var listener = self.makeListener()
    private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
    
    private var connections = [ClientConnection]()
    private var notificationConnections = [ALTDevice: NotificationConnection]()
    
    private init()
    {
        NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
    }
    
    func start()
    {
        switch self.state
        {
        case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
        default: break
        }
    }
    
    func stop()
    {
        switch self.state
        {
        case .running: self.listener.cancel()
        default: break
        }
    }
    
    func disconnect(_ connection: ClientConnection)
    {
        connection.disconnect()
        
        if let index = self.connections.firstIndex(where: { $0 === connection })
        {
            self.connections.remove(at: index)
        }
    }
}

private extension ConnectionManager
{
    func makeListener() -> NWListener
    {
        let listener = try! NWListener(using: .tcp)
        
        let service: NWListener.Service
        
        if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
        {
            let txtDictionary = ["serverID": serverID]
            let txtData = NetService.data(fromTXTRecord: txtDictionary)
            
            service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
        }
        else
        {
            service = NWListener.Service(type: ALTServerServiceType)
        }
        
        listener.service = service
        
        listener.serviceRegistrationUpdateHandler = { (serviceChange) in
            switch serviceChange
            {
            case .add(.service(let name, let type, let domain, _)):
                let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
                self.state = .running(service)
                
            default: break
            }
        }
        
        listener.stateUpdateHandler = { (state) in
            switch state
            {
            case .ready: break
            case .waiting, .setup: self.state = .connecting
            case .cancelled: self.state = .notRunning
            case .failed(let error):
                self.state = .failed(error)
                self.start()
                
            @unknown default: break
            }
        }
        
        listener.newConnectionHandler = { [weak self] (connection) in
            self?.prepare(connection)
        }
        
        return listener
    }
    
    func prepare(_ connection: NWConnection)
    {
        let clientConnection = ClientConnection(connection: .wireless(connection))
        
        guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
        self.connections.append(clientConnection)
        
        connection.stateUpdateHandler = { [weak self] (state) in
            switch state
            {
            case .setup, .preparing: break
                
            case .ready:
                print("Connected to client:", connection.endpoint)
                self?.handleRequest(for: clientConnection)
                
            case .waiting:
                print("Waiting for connection...")
                
            case .failed(let error):
                print("Failed to connect to service \(connection.endpoint).", error)
                self?.disconnect(clientConnection)
                
            case .cancelled:
                self?.disconnect(clientConnection)
                
            @unknown default: break
            }
        }
        
        connection.start(queue: self.dispatchQueue)
    }
}

private extension ConnectionManager
{
    func startNotificationConnection(to device: ALTDevice)
    {
        ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
            guard let connection = connection else { return }
            
            let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
            connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
                guard success else { return }
                
                connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
                    guard let self = self, let connection = connection else { return }
                    self.handle(notification, for: connection)
                }
                
                self.notificationConnections[device] = connection
            }
        }
    }
    
    func stopNotificationConnection(to device: ALTDevice)
    {
        guard let connection = self.notificationConnections[device] else { return }
        connection.disconnect()
        
        self.notificationConnections[device] = nil
    }
    
    func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
    {
        switch notification
        {
        case .wiredServerConnectionAvailableRequest:
            connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
                if let error = error, !success
                {
                    print("Error sending wired server connection response.", error)
                }
                else
                {
                    print("Sent wired server connection available response!")
                }
            }
            
        case .wiredServerConnectionStartRequest:
            ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
                if let wiredConnection = wiredConnection
                {
                    print("Started wired server connection!")
                    
                    let clientConnection = ClientConnection(connection: .wired(wiredConnection))
                    self.handleRequest(for: clientConnection)
                }
                else if let error = error
                {
                    print("Error starting wired server connection.", error)
                }
            }
            
        default: break
        }
    }
}

private extension ConnectionManager
{
    func handleRequest(for connection: ClientConnection)
    {
        connection.receiveRequest() { (result) in
            print("Received initial request with result:", result)
            
            switch result
            {
            case .failure(let error):
                let response = ErrorResponse(error: ALTServerError(error))
                connection.send(response, shouldDisconnect: true) { (result) in
                    print("Sent error response with result:", result)
                }
                
            case .success(.anisetteData(let request)):
                self.handleAnisetteDataRequest(request, for: connection)
                
            case .success(.prepareApp(let request)):
                self.handlePrepareAppRequest(request, for: connection)
                
            case .success:
                let response = ErrorResponse(error: ALTServerError(.unknownRequest))
                connection.send(response, shouldDisconnect: true) { (result) in
                    print("Sent unknown request response with result:", result)
                }
            }
        }
    }
    
    func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection)
    {
        AnisetteDataManager.shared.requestAnisetteData { (result) in
            switch result
            {
            case .failure(let error):
                let errorResponse = ErrorResponse(error: ALTServerError(error))
                connection.send(errorResponse, shouldDisconnect: true) { (result) in
                    print("Sent anisette data error response with result:", result)
                }
                
            case .success(let anisetteData):
                let response = AnisetteDataResponse(anisetteData: anisetteData)
                connection.send(response, shouldDisconnect: true) { (result) in
                    print("Sent anisette data response with result:", result)
                }
            }
        }
    }
    
    func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection)
    {
        var temporaryURL: URL?
        
        func finish(_ result: Result<Void, ALTServerError>)
        {
            if let temporaryURL = temporaryURL
            {
                do { try FileManager.default.removeItem(at: temporaryURL) }
                catch { print("Failed to remove .ipa.", error) }
            }
            
            switch result
            {
            case .failure(let error):
                print("Failed to process request from \(connection).", error)
                
                let response = ErrorResponse(error: ALTServerError(error))
                connection.send(response, shouldDisconnect: true) { (result) in
                    print("Sent install app error response to \(connection) with result:", result)
                }
                
            case .success:
                print("Processed request from \(connection).")
                
                let response = InstallationProgressResponse(progress: 1.0)
                connection.send(response, shouldDisconnect: true) { (result) in
                    print("Sent install app response to \(connection) with result:", result)
                }
            }
        }
        
        self.receiveApp(for: request, from: connection) { (result) in
            print("Received app with result:", result)
            
            switch result
            {
            case .failure(let error): finish(.failure(error))
            case .success(let fileURL):
                temporaryURL = fileURL
                
                print("Awaiting begin installation request...")
                
                connection.receiveRequest() { (result) in
                    print("Received begin installation request with result:", result)
                    
                    switch result
                    {
                    case .failure(let error): finish(.failure(error))
                    case .success(.beginInstallation):
                        print("Installing to device \(request.udid)...")
                        
                        self.installApp(at: fileURL, toDeviceWithUDID: request.udid, connection: connection) { (result) in
                            print("Installed to device with result:", result)
                            switch result
                            {
                            case .failure(let error): finish(.failure(error))
                            case .success: finish(.success(()))
                            }
                        }
                        
                    case .success:
                        let response = ErrorResponse(error: ALTServerError(.unknownRequest))
                        connection.send(response, shouldDisconnect: true) { (result) in
                            print("Sent unknown request error response to \(connection) with result:", result)
                        }
                    }
                }
            }
        }
    }
    
    func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
    {
        connection.receiveData(expectedBytes: request.contentSize) { (result) in
            do
            {
                print("Received app data!")
                
                let data = try result.get()
                                
                guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
                
                print("Writing app data...")
                
                let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
                try data.write(to: temporaryURL, options: .atomic)
                
                print("Wrote app to URL:", temporaryURL)
                
                completionHandler(.success(temporaryURL))
            }
            catch
            {
                print("Error processing app data:", error)
                
                completionHandler(.failure(ALTServerError(error)))
            }
        }
    }
    
    func installApp(at fileURL: URL, toDeviceWithUDID udid: String, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
    {
        let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
        var isSending = false
        
        var observation: NSKeyValueObservation?
        
        let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid) { (success, error) in
            print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
            
            if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
            {
                completionHandler(.failure(error))
            }
            else
            {
                completionHandler(.success(()))
            }
            
            observation?.invalidate()
            observation = nil
        }
        
        observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
            serialQueue.async {
                guard !isSending else { return }
                isSending = true
                
                print("Progress:", progress.fractionCompleted)
                let response = InstallationProgressResponse(progress: progress.fractionCompleted)
                
                connection.send(response) { (result) in                    
                    serialQueue.async {
                        isSending = false
                    }
                }
            }
        })
    }
}

private extension ConnectionManager
{
    @objc func deviceDidConnect(_ notification: Notification)
    {
        guard let device = notification.object as? ALTDevice else { return }
        self.startNotificationConnection(to: device)
    }
    
    @objc func deviceDidDisconnect(_ notification: Notification)
    {
        guard let device = notification.object as? ALTDevice else { return }
        self.stopNotificationConnection(to: device)
    }
}
