// 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 { Inputs, Input, Output } from "../output";
import * as resource from "../resource";
import * as settings from "../runtime/settings";
import * as serializeClosure from "../runtime/closure/serializeClosure";

/**
 * {@link CheckResult} represents the results of a call to {@link ResourceProvider.check}.
 */
export interface CheckResult<Inputs = any> {
    /**
     * The inputs to use, if any.
     */
    readonly inputs?: Inputs;

    /**
     * Any validation failures that occurred.
     */
    readonly failures?: CheckFailure[];
}

/**
 * {@link CheckFailure} represents a single failure in the results of a call to
 * {@link ResourceProvider.check}.
 */
export interface CheckFailure {
    /**
     * The property that failed validation.
     */
    readonly property: string;

    /**
     * The reason that the property failed validation.
     */
    readonly reason: string;
}

/**
 * {@link DiffResult} represents the results of a call to
 * {@link ResourceProvider.diff}.
 */
export interface DiffResult {
    /**
     * If true, this diff detected changes and suggests an update.
     */
    readonly changes?: boolean;

    /**
     * If this update requires a replacement, the set of properties triggering it.
     */
    readonly replaces?: string[];

    /**
     * An optional list of properties that will not ever change.
     */
    readonly stables?: string[];

    /**
     * If true, and a replacement occurs, the resource will first be deleted
     * before being recreated.  This is to avoid potential side-by-side issues
     * with the default create before delete behavior.
     */
    readonly deleteBeforeReplace?: boolean;
}

/**
 * {@link CreateResult} represents the results of a call to
 * {@link ResourceProvider.create}.
 */
export interface CreateResult<Outputs = any> {
    /**
     * The ID of the created resource.
     */
    readonly id: resource.ID;

    /**
     * Any properties that were computed during creation.
     */
    readonly outs?: Outputs;
}

/**
 * {@link ReadResult} represents the results of a call to
 * {@link ResourceProvider.read}.
 */
export interface ReadResult<Outputs = any> {
    /**
     * The ID of the resource ready back (or blank if missing).
     */
    readonly id?: resource.ID;
    /**
     * The current property state read from the live environment.
     */
    readonly props?: Outputs;
}

/**
 * {@link UpdateResult} represents the results of a call to
 * {@link ResourceProvider.update}.
 */
export interface UpdateResult<Outputs = any> {
    /**
     * Any properties that were computed during updating.
     */
    readonly outs?: Outputs;
}

/**
 * {@link ResourceProvider} represents an object that provides CRUD operations
 * for a particular type of resource.
 */
export interface ResourceProvider<Inputs = any, Outputs = any> {
    /**
     * Validates that the given property bag is valid for a resource of the given type.
     *
     * @param olds
     *  The old input properties to use for validation.
     * @param news
     *  The new input properties to use for validation.
     */
    check?: (olds: Inputs, news: Inputs) => Promise<CheckResult<Inputs>>;

    /**
     * Checks what impacts a hypothetical update will have on the resource's
     * properties.
     *
     * @param id
     *  The ID of the resource to diff.
     * @param olds
     *  The old values of properties to diff.
     * @param news
     *  The new values of properties to diff.
     */
    diff?: (id: resource.ID, olds: Outputs, news: Inputs) => Promise<DiffResult>;

    /**
     * Allocates a new instance of the provided resource and returns its unique
     * ID afterwards. If this call fails, the resource must not have been
     * created (i.e., it is "transactional").
     *
     * @param inputs
     *  The properties to set during creation.
     */
    create: (inputs: Inputs) => Promise<CreateResult<Outputs>>;

    /**
     * Reads the current live state associated with a resource. Enough state
     * must be included in the inputs to uniquely identify the resource; this is
     * typically just the resource ID, but it may also include some properties.
     */
    read?: (id: resource.ID, props?: Outputs) => Promise<ReadResult<Outputs>>;

    /**
     * Updates an existing resource with new values.
     *
     * @param id
     *  The ID of the resource to update.
     * @param olds
     *  The old values of properties to update.
     * @param news
     *  The new values of properties to update.
     */
    update?: (id: resource.ID, olds: Outputs, news: Inputs) => Promise<UpdateResult<Outputs>>;

    /**
     * Tears down an existing resource with the given ID.  If it fails,
     * the resource is assumed to still exist.
     *
     * @param id
     *  The ID of the resource to delete.
     * @param props
     *  The current properties on the resource.
     */
    delete?: (id: resource.ID, props: Outputs) => Promise<void>;
}

const providerCache = new WeakMap<ResourceProvider, Input<string>>();

function serializeProvider(provider: ResourceProvider): Input<string> {
    let result: Input<string>;
    // caching is enabled by default as of 3.0
    if (settings.cacheDynamicProviders()) {
        const cachedProvider = providerCache.get(provider);
        if (cachedProvider) {
            result = cachedProvider;
        } else {
            result = serializeFunctionMaybeSecret(provider);
            providerCache.set(provider, result);
        }
    } else {
        result = serializeFunctionMaybeSecret(provider);
    }
    return result;
}

function serializeFunctionMaybeSecret(provider: ResourceProvider): Output<string> {
    // Load runtime/closure on demand, as its dependencies are slow to load.
    //
    // See https://www.typescriptlang.org/docs/handbook/modules.html#optional-module-loading-and-other-advanced-loading-scenarios
    const sc: typeof serializeClosure = require("../runtime/closure/serializeClosure");

    const sfPromise = sc.serializeFunction(() => provider, { allowSecrets: true });

    // Create an Output from the promise's text and containsSecrets properties.  Uses the internal API since we don't provide a public interface for this.
    return new Output(
        [],
        sfPromise.then((sf) => sf.text),
        new Promise((resolve) => resolve(true)),
        sfPromise.then((sf) => sf.containsSecrets),
        new Promise((resolve) => resolve([])),
    );
}

/**
 * {@link Resource} represents a Pulumi resource that incorporates an inline
 * implementation of the Resource's CRUD operations.
 */
export abstract class Resource extends resource.CustomResource {
    /**
     * Creates a new dynamic resource.
     *
     * @param provider
     *  The implementation of the resource's CRUD operations.
     * @param name
     *  The name of the resource.
     * @param props
     *  The arguments to use to populate the new resource. Must not define the
     *  reserved property "__provider".
     * @param opts
     *  A bag of options that control this resource's behavior.
     * @param module
     *  The module of the resource.
     * @param type
     *  The type of the resource.
     */
    constructor(
        provider: ResourceProvider,
        name: string,
        props: Inputs,
        opts?: resource.CustomResourceOptions,
        module?: string,
        type: string = "Resource",
    ) {
        const providerKey: string = "__provider";
        if (props[providerKey]) {
            throw new Error("A dynamic resource must not define the __provider key");
        }
        props[providerKey] = serializeProvider(provider);

        super(`pulumi-nodejs:dynamic${module ? `/${module}` : ""}:${type}`, name, props, opts);
    }
}