import { createProvider } from 'hardhat/internal/core/providers/construction';
import { extendEnvironment, extendConfig, task } from 'hardhat/config';
import { ChainwebNetwork } from './utils/chainweb.js';
import {
  ChainwebExternalConfig,
  ChainwebExternalUserConfig,
  ChainwebInProcessConfig,
  ChainwebInProcessUserConfig,
  ChainwebPluginApi,
} from './type.js';
import {
  getKadenaExternalNetworks,
  getKadenaNetworks,
} from './utils/configure.js';
import { createGraph } from './utils/chainweb-graph.js';
import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider.js';
import Web3 from 'web3';
import { runRPCNode } from './server/runRPCNode.js';
import { CHAIN_ID_ADDRESS, VERIFY_ADDRESS } from './utils/network-contracts.js';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import picocolors from 'picocolors';
import { computeOriginHash, getNetworkStem } from './pure-utils.js';
import minimist from 'minimist';

extendConfig((config, userConfig) => {
  if (!userConfig.chainweb) {
    throw new Error(
      'hardhat_kadena plugins is imported but chainweb configuration is not presented in hardhat.config.js',
    );
  }
  if (Object.keys(userConfig.chainweb).length === 0) {
    throw new Error(
      'You need to provide at least one chainweb configuration in hardhat.config.js',
    );
  }

  const argv = minimist(process.argv.slice(2));

  config.defaultChainweb =
    argv['chainweb'] ??
    process.env['HK_ACTIVE_CHAINWEB_NAME'] ??
    userConfig.defaultChainweb ??
    'hardhat';

  const defaultChainwebChainIdOffset = 0;

  const hardhatConfig: ChainwebInProcessUserConfig = {
    chains: 2,
    chainwebChainIdOffset: defaultChainwebChainIdOffset,
    ...userConfig.chainweb.hardhat,
    type: 'in-process',
  };

  const localhostConfig: ChainwebExternalUserConfig = {
    chains: hardhatConfig.chains,
    chainIdOffset: hardhatConfig.chainIdOffset ?? 626000,
    externalHostUrl: 'http://localhost:8545',
    chainwebChainIdOffset: hardhatConfig.chainwebChainIdOffset,
    ...userConfig.chainweb['localhost'],
    type: 'external',
  };

  const userConfigWithLocalhost = {
    ...userConfig.chainweb,
    hardhat: hardhatConfig,
    localhost: localhostConfig,
  };

  if (!(config.defaultChainweb in userConfigWithLocalhost)) {
    throw new Error(
      `Default chainweb ${config.defaultChainweb} not found in hardhat.config.js`,
    );
  }

  Object.entries(userConfigWithLocalhost).forEach(
    ([name, chainwebUserConfig]) => {
      if (chainwebUserConfig === undefined) return;
      if (!chainwebUserConfig.chains) {
        throw new Error(
          'Number of chains is not presented in hardhat.config.js',
        );
      }
      let type = chainwebUserConfig.type ?? 'in-process';
      if (name === 'hardhat') {
        type = 'in-process';
      }
      if (name === 'localhost') {
        type = 'external';
      }

      const isDefaultChainweb = name === config.defaultChainweb;

      if (type === 'in-process') {
        const chainwebInProcessUserConfig =
          chainwebUserConfig as ChainwebInProcessUserConfig;

        if (chainwebInProcessUserConfig.graph) {
          const graph = chainwebInProcessUserConfig.graph ?? {};
          if (
            chainwebInProcessUserConfig.chains &&
            Object.keys(graph).length != chainwebInProcessUserConfig.chains
          ) {
            throw new Error(
              'Number of chains in graph does not match the graph configuration',
            );
          }
        }

        const offset = chainwebInProcessUserConfig.chainwebChainIdOffset ?? 0;

        const graphWithOffset = createGraph(
          chainwebInProcessUserConfig.chains,
        ).reduce(
          (acc, targets, source) => ({
            ...acc,
            [source + offset]: targets.map((target) => target + offset),
          }),
          {},
        );

        // add networks to hardhat

        const chainwebConfig: ChainwebInProcessConfig = {
          graph: chainwebInProcessUserConfig.graph ?? graphWithOffset,
          logging: 'info',
          type: 'in-process',
          chainIdOffset: 626000,
          accounts: config.networks.hardhat.accounts,
          precompiles: {
            chainwebChainId: CHAIN_ID_ADDRESS,
            spvVerify: VERIFY_ADDRESS,
          },
          chainwebChainIdOffset: defaultChainwebChainIdOffset,
          ...chainwebInProcessUserConfig,
        };

        const [networkConfig, etherscanCustomChains, etherscanApiKeys] =
          getKadenaNetworks({
            availableNetworks: userConfig.networks,
            hardhatNetwork: config.networks.hardhat,
            networkStem: getNetworkStem(name),
            numberOfChains: chainwebConfig.chains,
            accounts:
              chainwebConfig.accounts ??
              chainwebConfig.networkOptions?.accounts,
            loggingEnabled: chainwebConfig.logging === 'debug',
            forking: chainwebConfig.networkOptions?.forking?.url
              ? { enabled: true, ...chainwebConfig.networkOptions.forking }
              : undefined,
            networkOptions: chainwebConfig.networkOptions,
            chainIdOffset: chainwebConfig.chainIdOffset,
            chainwebChainIdOffset: chainwebConfig.chainwebChainIdOffset,
            etherscan: isDefaultChainweb ? chainwebConfig.etherscan : undefined,
          });

        config.networks = {
          ...config.networks,
          ...networkConfig,
        };
        if (isDefaultChainweb && chainwebConfig.etherscan) {
          config.etherscan = {
            apiKey: etherscanApiKeys,
            customChains: etherscanCustomChains,
            enabled: true,
          };
        }
        config.chainweb[name] = chainwebConfig;
      } else {
        const externalUserConfig =
          chainwebUserConfig as ChainwebExternalUserConfig;
        const chainwebConfig: ChainwebExternalConfig = {
          type: 'external',
          chainIdOffset: 626000,
          externalHostUrl: 'http://localhost:8545',
          accounts: 'remote',
          chainwebChainIdOffset: defaultChainwebChainIdOffset,
          ...externalUserConfig,
          precompiles: {
            chainwebChainId:
              externalUserConfig.precompiles?.chainwebChainId ??
              CHAIN_ID_ADDRESS,
            spvVerify:
              externalUserConfig.precompiles?.spvVerify ?? VERIFY_ADDRESS,
          },
        };

        const [networkConfig, etherscanCustomChains, etherscanApiKeys] =
          getKadenaExternalNetworks({
            availableNetworks: userConfig.networks,
            networkStem: getNetworkStem(name),
            numberOfChains: chainwebConfig.chains,
            accounts: chainwebConfig.accounts,
            baseUrl: chainwebConfig.externalHostUrl,
            networkOptions: chainwebConfig.networkOptions,
            chainIdOffset: chainwebConfig.chainIdOffset,
            chainwebChainIdOffset: chainwebConfig.chainwebChainIdOffset,
            etherscan: isDefaultChainweb ? chainwebConfig.etherscan : undefined,
          });
        // add networks to hardhat
        config.networks = {
          ...config.networks,
          ...networkConfig,
        };
        config.chainweb[name] = chainwebConfig;

        if (isDefaultChainweb && chainwebConfig.etherscan) {
          config.etherscan = {
            apiKey: etherscanApiKeys,
            customChains: etherscanCustomChains,
            enabled: true,
          };
        }
      }
    },
  );
});

const createExternalProvider = async (
  hre: HardhatRuntimeEnvironment,
  chainwebName: string,
): Promise<Omit<ChainwebPluginApi, 'initialize'>> => {
  const utils = await import('./utils.js');
  const networkStem = getNetworkStem(chainwebName);
  return {
    deployContractOnChains: utils.deployContractOnChains,
    getProvider: (cid: number) => {
      const name = `${networkStem}${cid}`;
      return createProvider(hre.config, name, hre.artifacts);
    },
    requestSpvProof: (targetChain, origin) =>
      utils.requestSpvProof(targetChain, origin),
    switchChain: async (cid: number | string) => {
      if (typeof cid === 'string') {
        await hre.switchNetwork(cid);
        console.log(`Switched to ${cid}`);
      } else {
        console.log(`Switched to ${networkStem}${cid}`);
        await hre.switchNetwork(`${networkStem}${cid}`);
      }
    },
    getChainIds: utils.getChainIds,
    callChainIdContract: utils.callChainIdContract,
    createTamperedProof: (targetChain, origin) =>
      utils.createTamperedProof(targetChain, origin),
    computeOriginHash: computeOriginHash,
    runOverChains: utils.runOverChains,
  };
};

const createInternalProvider = async (
  hre: HardhatRuntimeEnvironment,
  chainwebName: string,
  overrideForking?: { url: string; blockNumber?: number },
): Promise<Omit<ChainwebPluginApi, 'initialize'>> => {
  const chainweb = hre.config.chainweb[chainwebName];
  if (!chainweb || chainweb.type !== 'in-process') {
    throw new Error('Chainweb configuration not found');
  }
  const utils = await import('./utils.js');
  const networkStem = getNetworkStem(chainwebName);
  const chainwebNetwork = new ChainwebNetwork({
    chainweb,
    networks: hre.config.networks,
    chainwebName: chainwebName,
    overrideForking,
  });

  async function startHardhatNetwork() {
    await chainwebNetwork.start();
  }

  let stopped = false;
  async function stopHardhatNetwork() {
    if (stopped) return;

    await chainwebNetwork.stop();
    stopped = true;
    process.exit(0);
  }

  let setNetworkReady: () => void;
  const isNetworkReadyPromise = new Promise<void>((resolve) => {
    setNetworkReady = resolve;
  });

  let started = false;
  const spinupChainweb = async () => {
    if (started) return;
    started = true;
    process.on('exit', stopHardhatNetwork);
    process.on('SIGINT', stopHardhatNetwork);
    process.on('SIGTERM', stopHardhatNetwork);
    process.on('uncaughtException', stopHardhatNetwork);

    return startHardhatNetwork()
      .then(() => {
        setNetworkReady();
      })
      .catch(() => {
        process.exit(1);
      });
  };

  const originalSwitchNetwork = hre.switchNetwork;
  hre.switchNetwork = async (networkNameOrIndex: string | number) => {
    await isNetworkReadyPromise;
    const networkName =
      typeof networkNameOrIndex === 'number'
        ? `${networkStem}${networkNameOrIndex}`
        : networkNameOrIndex;
    if (networkName.startsWith(networkStem)) {
      const cid = parseInt(networkName.slice(networkStem.length));
      const provider = chainwebNetwork.getProvider(cid);
      hre.network.name = networkName;
      hre.network.config = hre.config.networks[networkName];
      hre.network.provider = provider;
      // update underlying library's provider data
      if ('ethers' in hre) {
        hre.ethers.provider = new HardhatEthersProvider(provider, networkName);
      }
      if ('web3' in hre) {
        hre.web3 = new Web3(provider);
      }
      console.log(`Switched to ${cid}`);
      return;
    }
    originalSwitchNetwork(networkName);
  };

  spinupChainweb();

  return {
    deployContractOnChains: utils.deployContractOnChains,
    getProvider: async (cid: number) => {
      await isNetworkReadyPromise;
      const provider = chainwebNetwork.getProvider(cid);
      return provider;
    },
    requestSpvProof: (targetChain, origin) =>
      utils.requestSpvProof(targetChain, origin, chainwebNetwork),
    switchChain: async (cid: number | string) => {
      await isNetworkReadyPromise;
      if (typeof cid === 'string') {
        await hre.switchNetwork(cid);
      } else {
        await hre.switchNetwork(`${networkStem}${cid}`);
      }
    },
    getChainIds: utils.getChainIds,
    callChainIdContract: utils.callChainIdContract,
    createTamperedProof: (targetChain, origin) =>
      utils.createTamperedProof(targetChain, origin, chainwebNetwork),
    computeOriginHash,
    runOverChains: utils.runOverChains,
  };
};

// const spinupChainweb = () =>
extendEnvironment((hre) => {
  let api: Omit<ChainwebPluginApi, 'initialize'> | undefined = undefined;
  let initDone = () => {};
  const init = new Promise<void>((resolve) => {
    initDone = resolve;
  });

  const safeCall =
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    <T extends () => (...args: any) => any>(cb: T) =>
      async (...args: Parameters<T>) => {
        await init;
        if (api !== undefined) {
          return cb()(...args);
        }
        throw new Error('Chainweb plugin not initialized');
      };

  hre.chainweb = {
    initialize: async (args) => {
      if (api) return;
      const chainweb = hre.config.chainweb[hre.config.defaultChainweb];
      if (!chainweb) {
        throw new Error('Chainweb configuration not found');
      }

      console.log(
        'Chainweb:',
        picocolors.bgGreenBright(` ${hre.config.defaultChainweb} `),
        'Chains:',
        picocolors.bgGreenBright(` ${chainweb.chains} `),
        '\n',
      );

      if (chainweb.type === 'external') {
        api = await createExternalProvider(hre, hre.config.defaultChainweb);
      } else {
        api = await createInternalProvider(
          hre,
          hre.config.defaultChainweb,
          args?.forking,
        );
      }
      initDone();
    },
    getProvider: safeCall(() => api!.getProvider),
    requestSpvProof: safeCall(() => api!.requestSpvProof),
    switchChain: safeCall(() => api!.switchChain),
    getChainIds: safeCall(() => api!.getChainIds),
    callChainIdContract: safeCall(() => api!.callChainIdContract),
    deployContractOnChains: safeCall(() => api!.deployContractOnChains),
    createTamperedProof: safeCall(() => api!.createTamperedProof),
    computeOriginHash,
    runOverChains: safeCall(() => api!.runOverChains),
  };
  if (process.env['HK_INIT_CHAINWEB'] === 'true') {
    hre.chainweb.initialize();
  }
});

const chainwebSwitch = ['chainweb', 'The name of the chainweb to use'] as const;

task(
  'node',
  `Starts a JSON-RPC server on top of Default Chainweb; use ${picocolors.bgBlackBright(' --network ')} if you want to run a single network rather than a chainweb`,
)
  .addOptionalParam(...chainwebSwitch)
  .setAction(async (taskArgs, hre, runSuper) => {
    const hasNetwork = process.argv.includes('--network');
    if (taskArgs.chainweb && hasNetwork) {
      console.error(
        'You can only specify one of chainweb or network, not both',
      );
      return;
    }
    if (hasNetwork) {
      return runSuper(taskArgs);
    }

    hre.config.defaultChainweb =
      taskArgs.chainweb ?? hre.config.defaultChainweb ?? 'hardhat';

    const config = hre.config.chainweb[hre.config.defaultChainweb];
    if (!config) {
      console.log(
        `Chainweb configuration ${hre.config.defaultChainweb} not found`,
      );
      return;
    }
    if (config.type === 'external') {
      console.error('You can only start a node for in-process chainweb');
      return;
    }

    let options:
      | undefined
      | { forking: { url: string; blockNumber?: number } } = undefined;

    if (taskArgs.fork) {
      options = {
        forking: {
          url: taskArgs.fork,
          blockNumber: taskArgs.forkBlockNumber,
        },
      };
    }

    await hre.chainweb.initialize(options);

    return runRPCNode(taskArgs, hre);
  });

task('test', `Run mocha tests; Supports Chainweb`)
  .addOptionalParam(...chainwebSwitch)
  .setAction(async (taskArgs, hre, runSuper) => {
    if (!hre.chainweb.initialize) {
      console.error('Chainweb _initialize is not a function');
      return;
    }
    hre.config.defaultChainweb =
      taskArgs.chainweb ?? hre.config.defaultChainweb ?? 'hardhat';

    hre.chainweb.initialize();

    if (!process.argv.includes('--network')) {
      const [first] = await hre.chainweb.getChainIds();
      await hre.chainweb.switchChain(first);
    }

    return runSuper(taskArgs);
  });

task(
  'run',
  `Runs a user-defined script after compiling the project; Supports Chainweb`,
)
  .addOptionalParam(...chainwebSwitch)
  .setAction(async (taskArgs, hre, runSuper) => {
    // Since hardhat run the script in a separate process, we need to set the following configurations
    // as environment variables then Hardhat forward them to the script process
    process.env['HK_ACTIVE_CHAINWEB_NAME'] = hre.config.defaultChainweb =
      taskArgs.chainweb ?? hre.config.defaultChainweb ?? 'hardhat';
    // then we know that the chainweb should run the initialization
    process.env['HK_INIT_CHAINWEB'] = 'true';
    return runSuper(taskArgs);
  });

task('print-config', 'print the final configuration')
  .addOptionalParam(...chainwebSwitch)
  .setAction(async (_taskArgs, hre) => {
    console.dir(hre.config, { depth: null, colors: true });
  });
