//
// 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 type { EmitterSubscription } from "react-native";
import { MalwarelyticsModule, MalwarelyticsModuleIfc, wrapNativeCall } from "./internal/MalwarelyticsModule";
import { EventHelper } from "./internal/EventHelper";
import type { MalwarelyticsConfig } from "./MalwarelyticsConfig";
import { MalwarelyticsRasp } from "./MalwarelyticsRasp";
import { MalwarelyticsAntivirus } from "./MalwarelyticsAntivirus";
import { MalwarelyticsAppleDetectableApp } from "./MalwarelyticsConfig";
import { Platform } from "react-native";
import { MalwarelyticsError } from "./MalwarelyticsError";

/**
 * JavaScript wrapper around native kotlin/swift Malwarelytics code.
 */
export class Malwarelytics {

    // Shared instance

    /**
     * Contains shared instance of Malwarelytics class.
     */
    static get sharedInstance(): Malwarelytics {
        if (this.instance === undefined) {
            this.instance = new Malwarelytics()
        }
        return this.instance
    }

    

    // Configuration

    /**
     * Get state of the Malwarelytics module.
     * 
     * This function also updates content of `initializationResult` getter, if result
     * is available.
     * 
     * @returns State of the module.
     */
    getState(): Promise<MalwarelyticsState> {
        return this.withModule(async (module) => {
            const stateWithResult = await module.getState()
            this.lastInitializationResult = stateWithResult.result
            return stateWithResult.state
        })
    }

    /** 
     * Apple specific: Obtain list of DetectableApp.KnownApp items that are predefined in native iOS library.
     *
     * @returns List of DetectableApp.KnownApp items.
     */
    getKnownDetectableApps(): Promise<MalwarelyticsAppleDetectableApp[]> {
        if (Platform.OS != "ios") {
            return Promise.reject(new MalwarelyticsError("METHOD_NOT_SUPPORTED", "This method is supported only on Apple platforms"))
        }
        return this.withModule(module => module.getKnownDetectableApps())   
    }

    /**
     * Initialize Malwarelytics instance with given configuration. 
     * 
     * Note that if you want to change the configuration on the running instance, 
     * then you have to `shutdown()` the instance first.
     * 
     * @param configuration Configuration to apply.
     */
    initialize(configuration: MalwarelyticsConfig): Promise<MalwarelyticsInitializationResult> {
        return this.withModule(async (module) => {
            this.lastInitializationResult = await module.initialize(configuration)
            return this.lastInitializationResult
        })
    }

    /**
     * Contains information about last initialization result.
     */
    get initializationResult(): MalwarelyticsInitializationResult | undefined {
        return this.lastInitializationResult
    }

    /**
     * Shutdown the previously initialized Malwarelytics instance.
     * @param clearAvUserId If `true`, then clears also persistent AV user ID, making the device appear as a newly created. The default value is `false`.
     */
    shutdown(clearAvUserId: boolean = false): Promise<void> {
        return this.withModule(async (module) => {
            await module.shutdown(clearAvUserId)
            this.lastInitializationResult = undefined
        })
    }

    /**
     * Get information whether module is already initialized. 
     */
    async isInitialized(): Promise<boolean> {
        return (await this.getState()) == "READY"
    }

    /**
     * Sets new client id that will be sent to the backend for user identification.
     * Pass `undefined` to remove the current client id (for example when user logged-out).
     * 
     * @param clientId Client id, `undefied` for deleting the value
     */
    setClientId(clientId: string | undefined): Promise<void> {
        return this.withModule((module) => module.setClientId(clientId))
    }

    /**
     * Sets new device id that will be sent to the backend for user identification.
     * Pass `undefied` to remove the current device id.
     * 
     * @param deviceId Device id, `undefied` for deleting the value
     */
    setDeviceId(deviceId: string | undefined): Promise<void> {
        return this.withModule((module) => module.setDeviceId(deviceId))
    }


    /**
     * Get AV User ID if it is available. The AV User ID will be available if the SDK initialized with 
     * result `SUCCESS`. Otherwise it might be undefined.
     */
    getAvUserId(): Promise<string | undefined> {
        return this.withModule(module => module.getAvUserId())   
    }

    // Events

    /**
     * Object representing a subscription to state events.
     */
    private stateEventsSubscription: EmitterSubscription | undefined

    /**
     * Set listener for Malwarelytic's instance state changes.
     * @param listener Listener implementation.
     */
    async setStateListener(listener: MalwarelyticsStateListener): Promise<void> {
        this.stateEventsSubscription?.remove()
        this.stateEventsSubscription = await this.eventHelper.addListener('Malwarelytics.STATE', (data) => {
            listener.malwarelyticsStateChanged(data as MalwarelyticsState)
        })
    }

    /**
     * Remove state listener previously set by `setStateListener()` method.
     */
    removeStateListener() {
        this.stateEventsSubscription?.remove()
        this.stateEventsSubscription = undefined
    }

    // Private methods and properties

    /**
     * Instance of native module.
     */
    private readonly nativeModule: MalwarelyticsModuleIfc
    /**
     * Instance of event helper. 
     */
    private readonly eventHelper: EventHelper
    /**
     * RASP module
     */
    readonly rasp: MalwarelyticsRasp
    /**
     * Antivirus module
     */
    readonly antivirus: MalwarelyticsAntivirus

    /**
     * Shared instnace of this class.
     */
    private static instance: Malwarelytics | undefined
    /**
     * Last initialization result
     */
    private lastInitializationResult: MalwarelyticsInitializationResult | undefined

    /**
     * Private constructor.
     */
    private constructor() {
        this.nativeModule = MalwarelyticsModule
        this.eventHelper = new EventHelper(this.nativeModule)
        this.rasp = new MalwarelyticsRasp(this.eventHelper)
        this.antivirus = new MalwarelyticsAntivirus(this.eventHelper)
    }

    /**
     * Execute closure with action with properly acquired instnace to native module.
     * @param action Action to execute.
     * @returns Result returned from the action.
     */
    private async withModule<T>(action: (nativeModule: MalwarelyticsModuleIfc) => Promise<T>): Promise<T> {
        return wrapNativeCall(this.nativeModule, action)
    }
}

/**
 * Type defining the state of Malwarelytics module:
 * - `"SHUTDOWN"` - Malwarelytics is not initialized.
 * - `"PENDING_INIT"` - Malwarelytics is initializing.
 * - `"READY"` - Malwarelytics is ready and prepared for use.
 * - `"PENDING_SHUTDOWN"` - Malwarelytics is shutting down.
 */
export type MalwarelyticsState = 'SHUTDOWN' | 'PENDING_INIT' | 'READY' | 'PENDING_SHUTDOWN';

/**
 * Type defining the result of Malwarelytics initialization.
 * - `'SUCCESS'` - Initialization completed successfully
 * - `'PERMANENT_OFFLINE_MODE'` - SDK works in permanent offline mode.
 * - `'TEMPORARY_OFFLINE_MODE'` - Android only: SDK works in temporary offline mode.
 */
export type MalwarelyticsInitializationResult = 'SUCCESS' | 'TEMPORARY_OFFLINE_MODE' | 'PERMANENT_OFFLINE_MODE';

/**
 * Interface for listening Malwarelytics module state changes.
 */
export interface MalwarelyticsStateListener {
    /**
     * Called when state of Malwarelytics module is changed.
     * @param state New module's state.
     */
    malwarelyticsStateChanged(state: MalwarelyticsState): void;
}