import LoggingUtils from "./LoggingUtils";
import { delay, timeoutOptDefaults } from "./TimingUtils";

/**
 * @description For situations where a Test Framework Exception (i.e., a locator timeout) could be incorrectly thrown based on the state of the Target-Application (i.e., a missing tail number). Use this to throw clearer 'Application Exception' errors under such circumstances.
 * @param actionVerb What are you trying to do? Could be 'gotoCatalog', 'addAnEnhancement', etc. Logged as `Attempting to ${actionVerb}`
 * @param failureExplenation Be descriptive. What is the context of the failure? If someone unfamiliar with the code base were to see this, how would they know if the error was caused by their code, or by an underlying problem with the Target Application?
 * @param actionToAttempt a lambda for the flaky action.
 * @returns
 */
export async function tryBusinessAction(actionVerb: string, failureExplenation: string, actionToAttempt: (...args: any[]) => Promise<any>): Promise<any> {
    let actionResult: any;
    console.log(`Attempting to ${actionVerb}...`)
    try {
        actionResult = await actionToAttempt();
    } catch (e) {
        if (e instanceof Error) {
            e.message = e.message + ">>Failure Explenation>> " + failureExplenation;
            throw (e);
        } else {
            throw (new Error("Unknown 'AttemptBusinessAction' State..."))
        }
    }
    console.log(`Successfully executed ${actionVerb}`)
    return actionResult;
}

/**
 * @description Brute-Force Flaky GUI activities by reseting and retrying.
 * @param actionToTry lambda for whatever flaky action you're trying to take
 * @param resetAction lambda for backing out of the problem and returning to a known state. Often, refreshing a browser, or closing a popup.
 * @param maxRetries number of times to retry the action
 * @param delayBetweenRetries milliseconds between retries
 */
export async function tryResetAndRetry(
    actionVerb: string,
    actionToTry: (...args: any[]) => Promise<any>,
    resetAction: (...args: any[]) => Promise<any>,
    maxRetries = 2,
    delayBetweenRetries = timeoutOptDefaults.timeoutMedium): Promise<any> {

    for (let i = 1; i <= maxRetries; i++) {
        try {
            await actionToTry()
        } catch {
            await delay(delayBetweenRetries);
            resetAction();
        }
    }
}

/**
 * @description For situations where a Test Framework Exception (i.e., a locator timeout) could be incorrectly thrown based on the state of the Target-Application (i.e., a missing tail number). Use this to throw clearer 'Application Exception' errors under such circumstances.
 * @param actionVerb What are you trying to do? Could be 'gotoCatalog', 'addAnEnhancement', etc. Logged as `Attempting to ${actionVerb}`
 * @param failureExplenation Be descriptive. What is the context of the failure? If someone unfamiliar with the code base were to see this, how would they know if the error was caused by their code, or by an underlying problem with the Target Application?
 * @param actionToAttempt a lambda for the flaky action.
 * @returns
 */
export async function tryBusinessActionWithRetries(actionVerb: string, failureExplenation: string, actionToAttempt: (...args: any[]) => Promise<any>, attempts = 3): Promise<any> {
    let actionResult: any;
    let isSuccessful: boolean = false;
    //actionVerb is already a logFunc string
    console.log(actionVerb);

    for (let i = 0; i < attempts; i++) {
        try {
            actionResult = await actionToAttempt();
            isSuccessful = true;
            break; // Break loop if actionToAttempt is successful
        } catch (e) {
            if (e instanceof Error) {
                e.message = e.message + ">>Failure Explanation>> " + failureExplenation;
            } else {
                throw (new Error("Unknown 'AttemptBusinessAction' State"))
            }
        }
    }

    if (!isSuccessful) {
        throw new Error(`Failed to ${actionVerb}`);
    }

    //actionVerb is already a logFunc string
    console.log(actionVerb);
    return actionResult;
}

/**
 * Tries to execute a block of code with chances to retry.
 * @param {Function} block The block of code to be executed.
 * @param {string} [blockDescription=''] A text description of the block (optional).
 * @param {number} [maxRetries=3] The maximum number of retry attempts (optional).
 * @param {number} [delayMs=1000] The delay between retry attempts in milliseconds (optional).
 */
export async function retry<T>(block: () => Promise<T>, blockDescription: string = '', maxRetries: number = 3, delayMs: number = 1000): Promise<T> {
    let retries: number = 0;
    if (!blockDescription.length) {
        blockDescription = block.toString();
    }

    while (retries < maxRetries) {
        console.log(LoggingUtils.logFunc(retry.name, blockDescription));
        try {
            return await block();
        } catch (error) {
            if (retries === maxRetries - 1) {
                throw new Error(`Max retries (${maxRetries}) exceeded. Last error: ${error}`);
            }
            // wait between retries
            await new Promise(resolve => setTimeout(resolve, delayMs));
            retries++;
        }
    }
    // This should not be reached, but just in case
    throw new Error(`Unexpected code execution. Max retries (${maxRetries}) exceeded.`);
}

export async function tryWithRetries(actionBlock: Function, attempts = 3, waitInterval = 1000) {
    for (let attempt = 1; attempt <= attempts; attempt++) {
        try {
            await actionBlock();
            if (attempt > 1)
                console.warn(`Had to retry but attempt ${attempt} succeeded!`);
            break; // Exit the loop if the action is successful
        } catch (error) {
            console.error(`Attempt ${attempt} failed!`);
            if (attempt < attempts) {
                await new Promise(resolve => setTimeout(resolve, waitInterval));
            }
        }
    }
}