// Copyright 2016-2022, Pulumi Corporation.
//
// 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 { AsyncLocalStorage } from "async_hooks";
import { ICallbackServer } from "./callbacks";
import * as config from "./config";
import { Stack } from "./stack";

import * as engrpc from "../proto/engine_grpc_pb";
import * as resrpc from "../proto/resource_grpc_pb";
import type { ResourceModule, ResourcePackage } from "./rpc";

const nodeEnvKeys = {
    project: "PULUMI_NODEJS_PROJECT",
    stack: "PULUMI_NODEJS_STACK",
    dryRun: "PULUMI_NODEJS_DRY_RUN",
    queryMode: "PULUMI_NODEJS_QUERY_MODE",
    parallel: "PULUMI_NODEJS_PARALLEL",
    monitorAddr: "PULUMI_NODEJS_MONITOR",
    engineAddr: "PULUMI_NODEJS_ENGINE",
    syncDir: "PULUMI_NODEJS_SYNC",

    // Unlike the values above, this value is not set by the CLI and is
    // controlled via a user-set environment variable.
    cacheDynamicProviders: "PULUMI_NODEJS_CACHE_DYNAMIC_PROVIDERS",

    organization: "PULUMI_NODEJS_ORGANIZATION",
};

const pulumiEnvKeys = {
    legacyApply: "PULUMI_ENABLE_LEGACY_APPLY",
};

/**
 * @internal
 */
export const asyncLocalStorage = new AsyncLocalStorage<Store>();

/**
 * @internal
 */
export interface WriteableOptions {
    /**
     * The name of the current project.
     */
    project?: string;

    /**
     * The name of the current stack being deployed into.
     */
    stack?: string;

    /**
     * The degree of parallelism for resource operations (default is serial).
     */
    parallel?: number;

    /**
     * A connection string to the engine's RPC, in case we need to reestablish.
     */
    engineAddr?: string;

    /**
     * A connection string to the monitor's RPC, in case we need to reestablish.
     */
    monitorAddr?: string;

    /**
     * Whether we are performing a preview (true) or a real deployment (false).
     */
    dryRun?: boolean;

    /**
     * True if we're in testing mode (allows execution without the CLI).
     */
    testModeEnabled?: boolean;

    /**
     * True if we're in query mode (does not allow resource registration).
     */
    queryMode?: boolean;

    /**
     * True if we will resolve missing outputs to inputs during preview.
     */
    legacyApply?: boolean;

    /**
     * True if we will cache serialized dynamic providers on the program side.
     */
    cacheDynamicProviders?: boolean;

    /**
     * The name of the current organization (if available).
     */
    organization?: string;

    /**
     * The number of process listeners which can be registered before writing a
     * warning.
     */
    maximumProcessListeners: number;

    /**
     * A directory containing the send/receive files for making synchronous
     * invokes to the engine.
     */
    syncDir?: string;
}

/**
 * @internal
 */
export interface Store {
    settings: {
        options: WriteableOptions;
        monitor?: resrpc.IResourceMonitorClient;
        engine?: engrpc.IEngineClient;
        rpcDone: Promise<any>;
        // Needed for legacy @pulumi/pulumi packages doing async feature checks.
        featureSupport: Record<string, boolean>;
    };
    config: Record<string, string>;
    stackResource?: Stack;
    leakCandidates: Set<Promise<any>>;
    logErrorCount: number;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * secrets across its RPC interface. When it does, we marshal outputs marked
     * with the secret bit in a special way.
     */
    supportsSecrets: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * resource references across its RPC interface. When it does, we marshal
     * resources in a special way.
     */
    supportsResourceReferences: boolean;

    /**
     * Tells u if the resource monitor we are connected to is able to support
     * output values across its RPC interface. When it does, we marshal outputs
     * in a special way.
     */
    supportsOutputValues: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * the `deletedWith` resource option across its RPC interface.
     */
    supportsDeletedWith: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * alias specs across its RPC interface. When it does, we marshal aliases in
     * a special way.
     */
    supportsAliasSpecs: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * remote transforms across its RPC interface. When it does, we marshal
     * transforms to the monitor instead of running them locally.
     */
    supportsTransforms: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * remote invoke transforms across its RPC interface. When it does, we marshal
     * transforms to the monitor instead of running them locally.
     */
    supportsInvokeTransforms: boolean;

    /**
     * Tells us if the resource monitor we are connected to is able to support
     * package references and parameterized providers.
     */
    supportsParameterization: boolean;

    /**
     * The callback service running for this deployment. This registers
     * callbacks and forwards them to the engine.
     */
    callbacks?: ICallbackServer;

    /**
     * Tracks the list of resource packages that have been registered.
     */
    resourcePackages: Map<string, ResourcePackage[]>;

    /**
     * Tracks the list of resource modules that have been registered.
     */
    resourceModules: Map<string, ResourceModule[]>;
}

/**
 * @internal
 */
export class LocalStore implements Store {
    settings = {
        options: {
            organization: process.env[nodeEnvKeys.organization],
            project: process.env[nodeEnvKeys.project] || "project",
            stack: process.env[nodeEnvKeys.stack] || "stack",
            dryRun: process.env[nodeEnvKeys.dryRun] === "true",
            queryMode: process.env[nodeEnvKeys.queryMode] === "true",
            monitorAddr: process.env[nodeEnvKeys.monitorAddr],
            engineAddr: process.env[nodeEnvKeys.engineAddr],
            syncDir: process.env[nodeEnvKeys.syncDir],
            cacheDynamicProviders: process.env[nodeEnvKeys.cacheDynamicProviders] !== "false",
            legacyApply: process.env[pulumiEnvKeys.legacyApply] === "true",
            maximumProcessListeners: 30,
        },
        rpcDone: Promise.resolve(),
        featureSupport: {},
    };
    config = {
        [config.configEnvKey]: process.env[config.configEnvKey] || "",
        [config.configSecretKeysEnvKey]: process.env[config.configSecretKeysEnvKey] || "",
    };
    stackResource = undefined;

    /**
     * Tracks the list of potential leak candidates.
     */
    leakCandidates = new Set<Promise<any>>();

    logErrorCount = 0;

    supportsSecrets = false;
    supportsResourceReferences = false;
    supportsOutputValues = false;
    supportsDeletedWith = false;
    supportsAliasSpecs = false;
    supportsTransforms = false;
    supportsInvokeTransforms = false;
    supportsParameterization = false;
    resourcePackages = new Map<string, ResourcePackage[]>();
    resourceModules = new Map<string, ResourceModule[]>();
}

/**
 * Get the root stack resource for the current stack deployment.
 *
 * @internal
 */
export function getStackResource(): Stack | undefined {
    const { stackResource } = getStore();
    return stackResource;
}

/**
 * Get the resource package map for the current stack deployment.
 *
 * @internal
 */
export function getResourcePackages(): Map<string, ResourcePackage[]> {
    const store = getGlobalStore();
    if (store.resourcePackages === undefined) {
        // resourcePackages can be undefined if an older SDK where it was not defined is created it.
        // In this case, we should initialize it to an empty map.
        store.resourcePackages = new Map<string, ResourcePackage[]>();
    }
    return store.resourcePackages;
}

/**
 * Get the resource module map for the current stack deployment.
 *
 * @internal
 */
export function getResourceModules(): Map<string, ResourceModule[]> {
    const store = getGlobalStore();
    if (store.resourceModules === undefined) {
        // resourceModules can be undefined if an older SDK where it was not defined is created it.
        // In this case, we should initialize it to an empty map.
        store.resourceModules = new Map<string, ResourceModule[]>();
    }
    return store.resourceModules;
}

/**
 * @internal
 */
export function setStackResource(newStackResource?: Stack) {
    const localStore = getStore();
    globalThis.stackResource = newStackResource;
    localStore.stackResource = newStackResource;
}

declare global {
    /* eslint-disable no-var */
    var globalStore: Store;
    var stackResource: Stack | undefined;
}

/**
 * @internal
 */
export function getLocalStore(): Store | undefined {
    return asyncLocalStorage.getStore();
}

(<any>getLocalStore).captureReplacement = () => {
    const returnFunc = () => {
        if (global.globalStore === undefined) {
            global.globalStore = new LocalStore();
        }
        return global.globalStore;
    };
    return returnFunc;
};

/**
 * @internal
 */
export const getStore = () => {
    const localStore = getLocalStore();
    if (localStore === undefined) {
        if (global.globalStore === undefined) {
            global.globalStore = new LocalStore();
        }
        return global.globalStore;
    }
    return localStore;
};

(<any>getStore).captureReplacement = () => {
    const returnFunc = () => {
        if (global.globalStore === undefined) {
            global.globalStore = new LocalStore();
        }
        return global.globalStore;
    };
    return returnFunc;
};

/**
 * @internal
 */
export const getGlobalStore = () => {
    if (global.globalStore === undefined) {
        global.globalStore = new LocalStore();
    }
    return global.globalStore;
};