//
// Copyright 2023 Wultra s.r.o.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions
// and limitations under the License.
//

import Foundation
import Dispatch
import AppProtection



/// Class implements `MalwarelyticsModule` object exposed to JavaScript.
@objc(Malwarelytics)
class Malwarelytics: RCTEventEmitter {
    
    // MARK: - RN integration & State
    
    /// Module's state
    enum State {
        case shutdown
        case pendingInit
        case ready
        case pendingShutdown
        
        var asString: String {
            switch self {
            case .shutdown:         return "SHUTDOWN"
            case .pendingInit:      return "PENDING_INIT"
            case .ready:            return "READY"
            case .pendingShutdown:  return "PENDING_SHUTDOWN"
            }
        }
    }
    
    /// Module's initialization result
    enum InitResult {
        case success
        case permanentOffline
        
        var asString: String {
            switch self {
            case .success:          return "SUCCESS"
            case .permanentOffline: return "PERMANENT_OFFLINE_MODE"
            }
        }
    }
    
    /// Event type
    enum Event {
        case state
        case rasp
        
        var asString: String {
            switch self {
            case .state:    return "Malwarelytics.STATE"
            case .rasp:     return "Malwarelytics.RASP"
            }
        }
    }
    
    
    /// Current state of the module
    private var state = State.shutdown
    
    /// Current initialization result.
    private var initResult: InitResult?
    
    /// A working queue that
    private let workingQueue = DispatchQueue(label: "MalwarelyticsRNBridge")
    
    override func invalidate() {
        super.invalidate()
        // Devmode reload
        workingQueue.async {
            self.stopService(clearAvUserId: false, notify: false)
        }
    }
    
    // MARK: - Events
    
    private let allEvents = [ Event.state, .rasp].map { $0.asString }
    
    override func supportedEvents() -> [String]! {
        return allEvents
    }
    
    /// Send event back to the JavaScript.
    /// - Parameters:
    ///   - event: Event type.
    ///   - body: Content of the event.
    func sendEvent(_ event: Event, body: Encodable) {
        do {
            sendEvent(event, body: try body.toJsObject())
        } catch {
            // TODO: dump error
        }        
    }
    
    /// Send event back to the JavaScript safely from the internal execution queue.
    /// - Parameters:
    ///   - event: Event type.
    ///   - body: Content of the event.
    ///   - checkState: If true, then check whether the module is in `.ready` state.
    func sendEvent(_ event: Event, body: Any, checkState: Bool = true) {
        workingQueue.async {
            if checkState && self.state != .ready {
                // Ignore the event if "ready" state is expected.
                return
            }
            self.sendEvent(withName: event.asString, body: body)
        }
    }
    
    
    // MARK: - Exported functions
    
    @objc(initialize:resolver:rejecter:)
    func initialize(_ config: Any?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject, expectedState: .shutdown) {
            let config = try checkDictionary(value: config, name: "configuration")
            let serviceConfig = try AppProtectionConfig.fromDictionary(config)
            try self.startService(config: serviceConfig)
            let result = config.hasObjectAt("apple.service") ? InitResult.success : .permanentOffline
            self.initResult = result
            return result.asString
        }
    }
    
    @objc(shutdown:resolver:rejecter:)
    func shutdown(_ clearAvUserId: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject) {
            self.stopService(clearAvUserId: clearAvUserId, notify: true)
        }
    }
    
    /// Structure contains module's state and initialization result.
    private struct StateWithResult: Encodable {
        let state: String
        let result: String?
    }
    
    @objc(getState:rejecter:)
    func getModuleState(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject, expectedState: nil) {
            return try StateWithResult(state: self.state.asString, result: self.initResult?.asString).toJsObject()
        }
    }

    @objc(getKnownDetectableApps:rejecter:)
    func getKnownDetectableApps(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        let allKnownApps: [DetectableApp] = [
            .KnownApps.anyDesk,
            .KnownApps.teamViewer,
            .KnownApps.logMeIn,
            .KnownApps.msRemoteDekstop,
            .KnownApps.jumpDesktop,
            .KnownApps.parallelsAccess,
            .KnownApps.chromeRemoteDesktop,
        ]
        do {
            resolve(try allKnownApps.toJsObject())
        } catch {
            error.report(to: reject)
        }
    }
    
    @objc(getSupportedEvents:rejecter:)
    func getSupportedEvents(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        resolve(self.supportedEvents())
    }
    
    @objc(getRaspInfo:resolver:rejecter:)
    func getRaspInfo(_ message: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject) {
            let message = try checkString(value: message, name: "message")
            return try self.getRaspInfo(service: self.service, infoType: message).toJsObject()
        }
    }
    
    @objc(setClientId:resolver:rejecter:)
    func setClientId(_ clientId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject) {
            guard let onlineService = try self.service.online else {
                throw ModuleError.notAvailable(message: "Function is not available in offline mode")
            }
            onlineService.clientIdentification.userId = try checkOptString(value: clientId, name: "clientId")
        }
    }
    
    @objc(setDeviceId:resolver:rejecter:)
    func setDeviceId(_ deviceId: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject) {
            guard let onlineService = try self.service.online else {
                throw ModuleError.notAvailable(message: "Function is not available in offline mode")
            }
            onlineService.clientIdentification.deviceId = try checkOptString(value: deviceId, name: "deviceId")
        }
    }
    
    @objc(getAvUserId:rejecter:)
    func getAvUserId(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        resolveOnQueue(resolve: resolve, reject: reject) {
            return try self.service.online?.avUid
        }
    }
    
    // MARK: - Native service
    
    /// Contains AppProtectionService instance. If instance is not available then throws error.
    private var service: AppProtectionService {
        get throws {
            guard let service = serviceInstance else {
                throw ModuleError.genericError(message: "Service is required, but it's not available")
            }
            return service
        }
    }
    /// Instance of AppProtectionService
    private var serviceInstance: AppProtectionService?
    
    /// Create and start AppProtectionService.
    /// - Parameter config: Instance of AppProtectionConfig.
    private func startService(config: AppProtectionConfig) throws {
        guard serviceInstance == nil else {
            throw ModuleError.genericError(message: "Service instance is already created")
        }
        changeState(.pendingInit)
        let service = AppProtectionService(config: config)
        service.rasp.addDelegate(self)
        serviceInstance = service
        changeState(.ready)
        // Notify RASP module about the initialization
        raspOnInit(service: service)
    }
    
    /// Stop AppProtectionService and release resources.
    /// - Parameters:
    ///   - clearAvUserId: If true, then also AV user ID will be reset.
    ///   - notify: If true, then send state notification to JavaScript.
    private func stopService(clearAvUserId: Bool, notify: Bool) {
        if (notify) {
            self.changeState(.pendingShutdown)
        }
        serviceInstance?.rasp.removeDelegate(self)
        if clearAvUserId, let onlineService = serviceInstance?.online {
            onlineService.resetInstanceId()
        }
        serviceInstance?.release()
        serviceInstance = nil
        initResult = nil
        if (notify) {
            self.changeState(.shutdown)
        } else {
            state = .shutdown
        }
    }
    
    
    // MARK: - Internal functions
    
    /// Function execute block on internal working queue and handle potential failures during the block execution. The value returned
    /// from the block is then used to resolve the JS promise.
    /// - Parameters:
    ///   - resolve: Resolve block.
    ///   - reject: Reject block.
    ///   - expectedState: If provided, then module must be in the expected state.
    ///   - block: Block to execute on working queue.
    private func resolveOnQueue<T>(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock, expectedState: State? = .ready, block: @escaping () throws -> T) {
        workingQueue.async {
            do {
                if let expectedState = expectedState {
                    guard expectedState == self.state else {
                        throw ModuleError.wrongState(current: self.state, expected: expectedState)
                    }
                }
                let result = try block()
                resolve(result)
            } catch {
                error.report(to: reject)
            }
        }
    }
    
    /// Function changes internal state of the module and notifies all state observers.
    /// - Parameter newState: New module's state.
    private func changeState(_ newState: State) {
        self.state = newState
        self.sendEvent(.state, body: newState.asString as NSString, checkState: false)
    }
}
