import {
    SimbaConfig,
    chooseApplicationFromList,
    getBlockchains,
    getStorages,
    primaryConstructorRequiresArgs,
    primaryConstructorInputs,
    authErrors,
} from '@simbachain/web3-suites';
// const log: Logger = new Logger({minLevel: "error"});
import {default as prompt} from 'prompts';
import {default as chalk} from 'chalk';
import axios from "axios";

interface DeploymentArguments {
    [key: string]: any;
}

interface DeploymentRequest {
    blockchain: string;
    app_name: string;
    args: DeploymentArguments;
    storage?: string;
    api_name?: string;
    display_name?: string;
    language?: string;
    code?: string;
    pre_txn_hook?: string;
    lib_name?: string;
}

/**
 * deploy a contract to your organisation and application on blocks
 * @param primary name of primary contract if user wants to skip the prompt to choose contract
 * @param deployInfo used to non-interactively deploy contract
 * @returns void | AxiosError
 */
export const deployContract = async (
    primary?: string,
    deployInfo?: Record<any, any>,
) => {
    SimbaConfig.log.debug(`:: ENTER :`);
    const config = new SimbaConfig();
    if (!config.ProjectConfigStore.has("contracts_info")) {
        SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : Please export your contract first with "npx hardhat simba export".`)}`);
        return;
    }

    const blockchainList = await getBlockchains(config);
    const storageList = await getStorages(config);

    if (!config.application) {
        try {
            await chooseApplicationFromList(config);
        } catch (e) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : ${JSON.stringify(e)}`)}`);
            return;
        }
    }

    const contractsInfo = SimbaConfig.ProjectConfigStore.get("contracts_info");

    if (!contractsInfo || !Object.keys(contractsInfo).length) {
        SimbaConfig.log.error(`${chalk.redBright(`\nsimba: no contracts present in your contracts_info in simba.json. Did you forget to export contracts first by running ${chalk.greenBright(`$ npx hardhat simba export`)} ?`)}`);
        return;
    }
    let contractName;
    if (!deployInfo) {
        if (primary) {
            if ((primary as string) in contractsInfo) {
                SimbaConfig.ProjectConfigStore.set('primary', primary);
                contractName = primary;
            } else {
                SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : Primary contract ${primary} is not the name of a contract in this project`)}`);
                return;
            }
        } else {
            const choices = [];
    
            for (const [contractName, _] of Object.entries(contractsInfo)) {
                choices.push({title: contractName, value: contractName});
            }
        
            const response = await prompt({
                type: 'select',
                name: 'contract_name',
                message: 'Please pick which contract you want to deploy',
                choices,
            });
        
            if (!response.contract_name) {
                SimbaConfig.log.error(`${chalk.redBright('\nsimba: EXIT : No contract selected for deployment!')}`);
                throw new Error('No Contract Selected!');
            }
        
            contractName = response.contract_name;
            SimbaConfig.ProjectConfigStore.set("primary", contractName);
        }
    }

    let chosen: any = {};
    let deployArgs: DeploymentArguments = {};
    let id;
    let _isLibrary: boolean = false;
    let sourceCode;
    if (!deployInfo) {
        const contractInfo = contractsInfo[contractName];
        sourceCode = contractInfo.source_code;
        const contractType = contractInfo.contract_type;
        _isLibrary = (contractType === "library") ? true : false;
        SimbaConfig.log.info(`${chalk.cyanBright(`\nsimba deploy: gathering info for deployment of contract ${chalk.greenBright(`${contractName}`)}`)}`)
        const questions: prompt.PromptObject[] = [
            {
                type: 'text',
                name: 'api',
                message: `Please enter an API name for contract ${chalk.greenBright(`${contractName}`)} [^[w-]*$]`,
                validate: (str: string): boolean => !!/^[\w-]*$/.exec(str),
            },
            {
                type: 'select',
                name: 'blockchain',
                message: 'Please choose the blockchain to deploy to.',
                choices: blockchainList,
                initial: 0,
            },
            {
                type: 'select',
                name: 'storage',
                message: 'Please choose the storage to use.',
                choices: storageList,
                initial: 0,
            },
        ];
        const constructorRequiresParams = await primaryConstructorRequiresArgs();
        const paramInputQuestions: any = [];
        let inputNameToTypeMap: any = {};
        let inputsAsJson = true;
        if (constructorRequiresParams) {
            const constructorInputs = await primaryConstructorInputs();
            const allParamsByJson = "enter all params as json object";
            const paramsOneByOne = "enter params one by one from prompts";
            const paramInputChoices = [paramsOneByOne, allParamsByJson];
            const paramChoices = [];
            for (let i = 0; i < paramInputChoices.length; i++) {
                const entry = paramInputChoices[i];
                paramChoices.push({
                    title: entry,
                    value: entry,
                });
            }
            const promptChosen = await prompt({
                type: 'select',
                name: 'input_method',
                message: 'Your constructor parameters can be input as either a single json object or one by one from prompts. Which would you prefer?',
                choices: paramChoices,
            });
    
            if (!promptChosen.input_method) {
                SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : no param input method chosen!`)}`)
                return;
            }
    
            if (promptChosen.input_method === allParamsByJson) {
                questions.push({
                    type: 'text',
                    name: 'args',
                    message: 'Please enter any arguments for the contract as a JSON dictionary.',
                    validate: (contractArgs: string): boolean => {
                        if (!contractArgs) {
                            return true;
                        } // Allow empty strings
                        try {
                            JSON.parse(contractArgs);
                            return true;
                        } catch {
                            return false;
                        }
                    },
                });
            } else {
                inputsAsJson = false;
                for (let i = 0; i < constructorInputs.length; i++) {
                    const inputEntry = constructorInputs[i];
                    const paramType = inputEntry.type;
                    const paramName = inputEntry.name;
                    inputNameToTypeMap[paramName] = paramType;
                    paramInputQuestions.push({
                        type: "text",
                        name: paramName,
                        message: `please input value for param ${chalk.greenBright(`${paramName}`)} of type ${chalk.greenBright(`${paramType}`)}`,
                    });
                }
            }
        }
    
        chosen = await prompt(questions);
    
        let inputsChosen = {} as any;
        if (!inputsAsJson) {
            inputsChosen = await prompt(paramInputQuestions);
            SimbaConfig.log.debug(`:: inputsChosen : ${JSON.stringify(inputsChosen)}`);
            for (const key in inputsChosen) {
                if (!inputNameToTypeMap[key].startsWith("string") || !inputNameToTypeMap[key].startsWith("address")) {
                    try {
                        // trying and catching. there are custom data types that users can define
                        // that we won't be able to anticipate. so we try to parse those,
                        // and if they're really just extensions of 'string', then we continue
                        inputsChosen[key] = JSON.parse(inputsChosen[key]);
                    } catch (e) {
                        continue;
                    }
                }
            }
        }
    
        if (!chosen.api) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : No API Name chosen!`)}`);
            return;
        }
    
        if (!chosen.blockchain) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT :  No blockchain chosen!`)}`);
            return;
        }
    
        if (!chosen.storage) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : No storage chosen!`)}`)
            return;
        }
    
        if (constructorRequiresParams && !chosen.args && !inputsChosen) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : Your contract requires constructor arguments`)}`)
            return;
        }
        id = contractInfo.design_id;
        if (chosen.args) {
            deployArgs = JSON.parse(chosen.args) as DeploymentArguments;
        } else {
            if (config.ProjectConfigStore.has('defaultArgs')) {
                deployArgs = config.ProjectConfigStore.get('defaultArgs') as DeploymentArguments;
            } else {
                if (inputsChosen) {
                    deployArgs = JSON.parse(JSON.stringify(inputsChosen));
                }
            }
        }
    }


    let deployURL;
    let deployment: DeploymentRequest;

    if (deployInfo) {
        contractName = deployInfo.api;
        deployURL = deployInfo.url;
        deployment = {
            blockchain: deployInfo.blockchain,
            storage: deployInfo.storage,
            api_name: deployInfo.api,
            app_name: config.application.name,
            display_name: config.application.name,
            args: deployInfo.args,
        }
    } else {
        if (_isLibrary) {
            deployURL = `v2/organisations/${config.organisation.id}/deployed_artifacts/create/`;
            const b64CodeBuffer = Buffer.from(sourceCode)
            const base64CodeString = b64CodeBuffer.toString('base64')
            deployment = {
                args: deployArgs,
                language: "Solidity",
                code: base64CodeString,
                blockchain: chosen.blockchain,
                app_name: config.application.name,
                lib_name: config.ProjectConfigStore.get("primary"),
            };
        } else {
            deployURL = `v2/organisations/${config.organisation.id}/contract_designs/${id}/deploy/`;
            deployment = {
                blockchain: chosen.blockchain,
                storage: chosen.storage,
                api_name: chosen.api,
                app_name: config.application.name,
                display_name: config.application.name,
                args: deployArgs,
            };
        }
    }

    const authStore = await SimbaConfig.authStore();
    if (!authStore) {
        SimbaConfig.log.error(`${chalk.redBright(`\nsimba: no authStore created. Please make sure your baseURL is properly configured in your simba.json`)}`);
        return Promise.reject(new Error(authErrors.badAuthProviderInfo));
    }
    try {
        const resp = await authStore.doPostRequest(
            deployURL,
            deployment,
            "application/json",
            true,
        );
        SimbaConfig.log.debug(`:: resp : ${JSON.stringify(resp)}`);
        if (!resp) {
            SimbaConfig.log.error(`${chalk.redBright(`simba: EXIT : error deploying contract`)}`);
            return;
        }
        const deployment_id = resp.deployment_id;
        const transaction_hash = resp.transaction_hash;
        config.ProjectConfigStore.set('deployment_id', deployment_id);
        SimbaConfig.log.info(`${chalk.cyanBright(`\nsimba deploy: Contract deployment ID for contract ${contractName}:`)} ${chalk.greenBright(`${deployment_id}`)}`);
        SimbaConfig.log.info(`${chalk.cyanBright(`\nsimba deploy: txn hash for contract ${contractName}:`)} ${chalk.greenBright(`${transaction_hash}`)}`);

        let deployed = false;
        let lastState = null;
        let retVal = null;

        do {
            const checkDeployURL = `v2/organisations/${config.organisation.id}/deployments/${deployment_id}/`;
            const check_resp = await authStore.doGetRequest(
                checkDeployURL,
            ) as Record<any, any>;
            if (!check_resp) {
                SimbaConfig.log.error(`${chalk.redBright(`simba: EXIT : error checking deployment URL`)}`);
                return;
            }

            const state: any = check_resp.state;
            SimbaConfig.log.debug(`:: state : ${state}`);

            switch (state) {
                case 'INITIALISED':
                    if (lastState !== state) {
                        lastState = state;
                        SimbaConfig.log.info(
                            `${chalk.cyanBright('\nsimba deploy: Your contract deployment has been initialised...')}`,
                        );
                    }
                    break;
                case 'EXECUTING':
                    if (lastState !== state) {
                        lastState = state;
                        SimbaConfig.log.info(`${chalk.cyanBright('\nsimba deploy: deployment is executing...')}`);
                    }
                    break;
                case 'COMPLETED':
                    deployed = true;
                    const contractName = config.ProjectConfigStore.get("primary");
                    const contractsInfo = config.ProjectConfigStore.get("contracts_info") ?
                        config.ProjectConfigStore.get("contracts_info") :
                        {};
                    contractsInfo[contractName] = contractsInfo[contractName] ?
                    contractsInfo[contractName] :
                    {};
                    contractsInfo[contractName].application = SimbaConfig.application.name;
                    if (!_isLibrary) {
                        const contractAddress = check_resp.primary.address;
                        contractsInfo[contractName].address = contractAddress;
                        contractsInfo[contractName].deployment_id = deployment_id;
                        contractsInfo[contractName].transaction_hash = transaction_hash;
                        config.ProjectConfigStore.set("contracts_info", contractsInfo);
                        const most_recent_deployment_info = {
                            address: contractAddress,
                            transaction_hash,
                            deployment_id,
                            type: "contract"
                        };
                        config.ProjectConfigStore.set('most_recent_deployment_info', most_recent_deployment_info);
                        SimbaConfig.log.info(
                            `${chalk.cyanBright(`\nsimba deploy: contract ${chalk.greenBright(`${contractName}`)} was deployed to ${chalk.greenBright(`${contractAddress}`)} with deployment_id ${chalk.greenBright(`${deployment_id}`)} and transaction_hash ${chalk.greenBright(`${transaction_hash}`)}. Information pertaining to this deployment can be found in your simba.json under contracts_info.${contractName}.`)}`,
                        );
                    } else {
                        const deploymentInfo = check_resp.deployment;
                        for (let i = 0; i < deploymentInfo.length; i++) {
                            const entry = deploymentInfo[i];
                            if (!(entry.name === contractName)) {
                                continue;
                            }
                            const libraryAddress = entry.address;
                            let contractsInfo = config.ProjectConfigStore.get("contracts_info") as any;
                            contractsInfo[contractName].address = libraryAddress;
                            contractsInfo[contractName].deployment_id = deployment_id;
                            contractsInfo[contractName].transaction_hash = transaction_hash;
                            config.ProjectConfigStore.set("contracts_info", contractsInfo);
                            const most_recent_deployment_info = {
                                address: libraryAddress,
                                deployment_id,
                                transaction_hash,
                                type: "library",
                            };
                            const libraryAddresses = config.ProjectConfigStore.get("library_addresses") ?
                                config.ProjectConfigStore.get("library_addresses") :
                                {};
                            libraryAddress[contractName] = libraryAddress;
                            config.ProjectConfigStore.set("library_addresses", libraryAddresses);
                            config.ProjectConfigStore.set("most_recent_deployment_info", most_recent_deployment_info);
                            SimbaConfig.log.info(`${chalk.cyanBright(`simba: your library was deployed to address ${chalk.greenBright(`${libraryAddress}`)}, with deployment_id ${chalk.greenBright(`${deployment_id}`)} and transaction_hash ${chalk.greenBright(`${transaction_hash}`)}. Information pertaining to this deployment can be found in your simba.json`)}`);
                        }
                    }
                    break;
                case 'ABORTED':
                    deployed = true;
                    SimbaConfig.log.error(`${chalk.red('\nsimba deploy: Your contract deployment was aborted...')}`);
                    SimbaConfig.log.error(`${chalk.red(`\nsimba deploy: EXIT : ${check_resp.error}`)}${check_resp.error}`);
                    SimbaConfig.log.debug(`:: EXIT :`);
                    retVal = new Error(check_resp.error);
                    break;
            }
        } while (!deployed);

        Promise.resolve(retVal);
    }  catch (error) {
        if (axios.isAxiosError(error) && error.response) {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : ${JSON.stringify(error.response.data)}`)}`);
            return error;
        } else {
            SimbaConfig.log.error(`${chalk.redBright(`\nsimba: EXIT : ${JSON.stringify(error)}`)}`);
            return error;
        }
    }
    SimbaConfig.log.debug(`:: EXIT :`);
    return;
}