import type {
  ConfigurationVariableResolver,
  EdrNetworkAccountsConfig,
  EdrNetworkAccountsUserConfig,
  ChainDescriptorConfig,
  ChainDescriptorsConfig,
  ChainDescriptorsUserConfig,
  EdrNetworkConfig,
  EdrNetworkForkingConfig,
  EdrNetworkForkingUserConfig,
  EdrNetworkMiningConfig,
  EdrNetworkMiningUserConfig,
  EdrNetworkUserConfig,
  GasConfig,
  GasUserConfig,
  HttpNetworkAccountsConfig,
  HttpNetworkAccountsUserConfig,
  HttpNetworkConfig,
  HttpNetworkUserConfig,
} from "../../../types/config.js";
import type { ChainType } from "../../../types/network.js";

import path from "node:path";

import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
import {
  hexStringToBytes,
  normalizeHexString,
} from "@nomicfoundation/hardhat-utils/hex";
import { deepClone, deepMerge } from "@nomicfoundation/hardhat-utils/lang";

import { GENERIC_CHAIN_TYPE } from "../../constants.js";

import { DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS } from "./accounts/constants.js";
import { DEFAULT_CHAIN_DESCRIPTORS } from "./chain-descriptors.js";
import {
  DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS,
  EDR_NETWORK_DEFAULT_COINBASE,
} from "./edr/edr-provider.js";
import { getCurrentHardfork } from "./edr/types/hardfork.js";
import { isHttpNetworkHdAccountsUserConfig } from "./type-validation.js";

export function resolveHttpNetwork(
  networkConfig: HttpNetworkUserConfig,
  resolveConfigurationVariable: ConfigurationVariableResolver,
): HttpNetworkConfig {
  return {
    type: "http",
    accounts: resolveHttpNetworkAccounts(
      networkConfig.accounts,
      resolveConfigurationVariable,
    ),
    chainId: networkConfig.chainId,
    chainType: networkConfig.chainType,
    from: networkConfig.from,
    gas: resolveGasConfig(networkConfig.gas),
    gasMultiplier: networkConfig.gasMultiplier ?? 1,
    gasPrice: resolveGasConfig(networkConfig.gasPrice),
    url: resolveConfigurationVariable(networkConfig.url),
    timeout: networkConfig.timeout ?? 300_000,
    httpHeaders: networkConfig.httpHeaders ?? {},
  };
}

export function resolveEdrNetwork(
  networkConfig: EdrNetworkUserConfig,
  defaultChainType: ChainType,
  cachePath: string,
  resolveConfigurationVariable: ConfigurationVariableResolver,
): EdrNetworkConfig {
  return {
    type: "edr-simulated",
    accounts: resolveEdrNetworkAccounts(
      networkConfig.accounts,
      resolveConfigurationVariable,
    ),
    chainId: networkConfig.chainId ?? 31337,
    chainType: networkConfig.chainType,
    from: networkConfig.from,
    gas: resolveGasConfig(networkConfig.gas),
    gasMultiplier: networkConfig.gasMultiplier ?? 1,
    gasPrice: resolveGasConfig(networkConfig.gasPrice),

    allowBlocksWithSameTimestamp:
      networkConfig.allowBlocksWithSameTimestamp ?? false,
    allowUnlimitedContractSize:
      networkConfig.allowUnlimitedContractSize ?? false,
    blockGasLimit: BigInt(networkConfig.blockGasLimit ?? 60_000_000n),
    coinbase: resolveCoinbase(networkConfig.coinbase),

    forking: resolveForkingConfig(
      networkConfig.forking,
      cachePath,
      resolveConfigurationVariable,
    ),
    hardfork: resolveHardfork(
      networkConfig.hardfork,
      networkConfig.chainType ?? defaultChainType,
    ),
    initialBaseFeePerGas: resolveInitialBaseFeePerGas(
      networkConfig.initialBaseFeePerGas,
    ),
    initialDate: networkConfig.initialDate ?? new Date(),
    loggingEnabled: networkConfig.loggingEnabled ?? false,
    minGasPrice: BigInt(networkConfig.minGasPrice ?? 0),
    mining: resolveMiningConfig(networkConfig.mining),
    networkId: networkConfig.networkId ?? networkConfig.chainId ?? 31337,
    throwOnCallFailures: networkConfig.throwOnCallFailures ?? true,
    throwOnTransactionFailures:
      networkConfig.throwOnTransactionFailures ?? true,
  };
}

export function resolveGasConfig(value: GasUserConfig = "auto"): GasConfig {
  return value === "auto" ? value : BigInt(value);
}

export function resolveHttpNetworkAccounts(
  accounts: HttpNetworkAccountsUserConfig | undefined = "remote",
  resolveConfigurationVariable: ConfigurationVariableResolver,
): HttpNetworkAccountsConfig {
  if (Array.isArray(accounts)) {
    return accounts.map((acc) => {
      if (typeof acc === "string") {
        acc = normalizeHexString(acc);
      }

      return resolveConfigurationVariable(acc);
    });
  }

  if (isHttpNetworkHdAccountsUserConfig(accounts)) {
    const { passphrase: defaultPassphrase, ...defaultHdAccountRest } =
      DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS;
    const { mnemonic, passphrase, ...hdAccountRest } = accounts;

    return {
      ...defaultHdAccountRest,
      ...hdAccountRest,
      mnemonic: resolveConfigurationVariable(mnemonic),
      passphrase: resolveConfigurationVariable(passphrase ?? defaultPassphrase),
    };
  }

  return accounts;
}

export function resolveEdrNetworkAccounts(
  accounts:
    | EdrNetworkAccountsUserConfig
    | undefined = DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS,
  resolveConfigurationVariable: ConfigurationVariableResolver,
): EdrNetworkAccountsConfig {
  if (Array.isArray(accounts)) {
    return accounts.map(({ privateKey, balance }) => {
      if (typeof privateKey === "string") {
        privateKey = normalizeHexString(privateKey);
      }

      return {
        privateKey: resolveConfigurationVariable(privateKey),
        balance: BigInt(balance),
      };
    });
  }

  const {
    mnemonic: defaultMnemonic,
    accountsBalance: defaultAccountsBalance,
    passphrase: defaultPassphrase,
    ...defaultHdAccountRest
  } = DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS;
  const { mnemonic, passphrase, accountsBalance, ...hdAccountRest } = accounts;
  return {
    ...defaultHdAccountRest,
    ...hdAccountRest,
    mnemonic: resolveConfigurationVariable(mnemonic ?? defaultMnemonic),
    accountsBalance: BigInt(accountsBalance ?? defaultAccountsBalance),
    passphrase: resolveConfigurationVariable(passphrase ?? defaultPassphrase),
  };
}

export function resolveForkingConfig(
  forkingUserConfig: EdrNetworkForkingUserConfig | undefined,
  cacheDir: string,
  resolveConfigurationVariable: ConfigurationVariableResolver,
): EdrNetworkForkingConfig | undefined {
  if (forkingUserConfig === undefined) {
    return undefined;
  }

  return {
    enabled: forkingUserConfig.enabled ?? true,
    url: resolveConfigurationVariable(forkingUserConfig.url),
    cacheDir: path.join(cacheDir, "edr-fork-cache"),
    blockNumber:
      forkingUserConfig.blockNumber !== undefined
        ? BigInt(forkingUserConfig.blockNumber)
        : undefined,
    httpHeaders: forkingUserConfig.httpHeaders,
  };
}

export function resolveMiningConfig(
  miningUserConfig: EdrNetworkMiningUserConfig | undefined = {},
): EdrNetworkMiningConfig {
  const { auto, interval, mempool } = miningUserConfig;

  return {
    auto: auto ?? interval === undefined,
    interval: interval ?? 0,
    mempool: {
      order: mempool?.order ?? "priority",
    },
  };
}

export function resolveCoinbase(
  coinbase: string | undefined = EDR_NETWORK_DEFAULT_COINBASE,
): Uint8Array {
  return hexStringToBytes(coinbase);
}

export async function resolveChainDescriptors(
  chainDescriptors: ChainDescriptorsUserConfig | undefined,
): Promise<ChainDescriptorsConfig> {
  const resolvedChainDescriptors: ChainDescriptorsConfig = await deepClone(
    DEFAULT_CHAIN_DESCRIPTORS,
  );

  if (chainDescriptors === undefined) {
    return resolvedChainDescriptors;
  }

  // Loop over the user-provided chain descriptors
  // and merge them with the default ones
  for (const [chainId, userDescriptor] of Object.entries(chainDescriptors)) {
    const existingDescriptor: ChainDescriptorConfig =
      resolvedChainDescriptors.get(toBigInt(chainId)) ?? {
        name: userDescriptor.name,
        chainType: GENERIC_CHAIN_TYPE,
        blockExplorers: {},
      };

    existingDescriptor.name = userDescriptor.name;

    if (userDescriptor.chainType !== undefined) {
      existingDescriptor.chainType = userDescriptor.chainType;
    }

    if (userDescriptor.hardforkHistory !== undefined) {
      existingDescriptor.hardforkHistory = new Map(
        Object.entries(userDescriptor.hardforkHistory),
      );
    }

    // Merge block explorers configs so users can override individual fields
    // without losing the others. Also allows adding new explorers not present
    // in the defaults. We cast to Record<string, any> because explorerConfig
    // can be different types (EtherscanUserConfig, BlockscoutUserConfig, etc.)
    // and we need to handle them uniformly for extensibility.
    const existingBlockExplorers: Record<string, any> =
      existingDescriptor.blockExplorers;
    for (const [explorerName, explorerConfig] of Object.entries(
      userDescriptor.blockExplorers ?? {},
    )) {
      existingBlockExplorers[explorerName] = deepMerge(
        existingBlockExplorers[explorerName] ?? {},
        explorerConfig,
        false,
      );
    }

    resolvedChainDescriptors.set(toBigInt(chainId), existingDescriptor);
  }

  return resolvedChainDescriptors;
}

export function resolveHardfork(
  hardfork: string | undefined,
  chainType: ChainType,
): string {
  if (hardfork !== undefined) {
    return hardfork;
  }

  return getCurrentHardfork(chainType);
}

export function resolveInitialBaseFeePerGas(
  initialBaseFeePerGas: bigint | number | undefined,
): bigint | undefined {
  return initialBaseFeePerGas !== undefined
    ? BigInt(initialBaseFeePerGas)
    : undefined;
}
