/* eslint-disable no-restricted-syntax -- hack */
import type {
  EdrNetworkAccountConfig,
  EdrNetworkAccountsConfig,
  ChainDescriptorsConfig,
  EdrNetworkForkingConfig,
  EdrNetworkMempoolConfig,
  EdrNetworkMiningConfig,
} from "../../../../../types/config.js";
import type { ChainType } from "../../../../../types/network.js";
import type { GasMeasurement } from "../../../gas-analytics/types.js";
import type {
  IntervalRange,
  ChainOverride,
  ForkConfig,
  GasReport,
} from "@nomicfoundation/edr";

import {
  GasReportExecutionStatus,
  MineOrdering,
  OpHardfork,
  SpecId,
  FRONTIER,
  HOMESTEAD,
  DAO_FORK,
  TANGERINE,
  SPURIOUS_DRAGON,
  BYZANTIUM,
  CONSTANTINOPLE,
  PETERSBURG,
  ISTANBUL,
  MUIR_GLACIER,
  BERLIN,
  LONDON,
  ARROW_GLACIER,
  GRAY_GLACIER,
  MERGE,
  SHANGHAI,
  CANCUN,
  PRAGUE,
  OSAKA,
  BEDROCK,
  REGOLITH,
  CANYON,
  ECOTONE,
  FJORD,
  GRANITE,
  HOLOCENE,
  ISTHMUS,
} from "@nomicfoundation/edr";

import {
  GENERIC_CHAIN_TYPE,
  L1_CHAIN_TYPE,
  OPTIMISM_CHAIN_TYPE,
} from "../../../../constants.js";
import { FixedValueConfigurationVariable } from "../../../../core/configuration-variables.js";
import { derivePrivateKeys } from "../../accounts/derive-private-keys.js";
import {
  DEFAULT_EDR_NETWORK_BALANCE,
  EDR_NETWORK_DEFAULT_PRIVATE_KEYS,
  isDefaultEdrNetworkHDAccountsConfig,
} from "../edr-provider.js";
import { L1HardforkName, OpHardforkName } from "../types/hardfork.js";

import { getL1HardforkName, getOpHardforkName } from "./hardfork.js";

export function edrL1HardforkToHardhatL1HardforkName(
  hardfork: SpecId,
): L1HardforkName {
  switch (hardfork) {
    case SpecId.Frontier:
      return L1HardforkName.FRONTIER;
    case SpecId.FrontierThawing:
      return L1HardforkName.FRONTIER;
    case SpecId.Homestead:
      return L1HardforkName.HOMESTEAD;
    case SpecId.DaoFork:
      return L1HardforkName.DAO;
    case SpecId.Tangerine:
      return L1HardforkName.TANGERINE_WHISTLE;
    case SpecId.SpuriousDragon:
      return L1HardforkName.SPURIOUS_DRAGON;
    case SpecId.Byzantium:
      return L1HardforkName.BYZANTIUM;
    case SpecId.Constantinople:
      return L1HardforkName.CONSTANTINOPLE;
    case SpecId.Petersburg:
      return L1HardforkName.PETERSBURG;
    case SpecId.Istanbul:
      return L1HardforkName.ISTANBUL;
    case SpecId.MuirGlacier:
      return L1HardforkName.MUIR_GLACIER;
    case SpecId.Berlin:
      return L1HardforkName.BERLIN;
    case SpecId.London:
      return L1HardforkName.LONDON;
    case SpecId.ArrowGlacier:
      return L1HardforkName.ARROW_GLACIER;
    case SpecId.GrayGlacier:
      return L1HardforkName.GRAY_GLACIER;
    case SpecId.Merge:
      return L1HardforkName.MERGE;
    case SpecId.Shanghai:
      return L1HardforkName.SHANGHAI;
    case SpecId.Cancun:
      return L1HardforkName.CANCUN;
    case SpecId.Prague:
      return L1HardforkName.PRAGUE;
    case SpecId.Osaka:
      return L1HardforkName.OSAKA;
    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- trust but verify
    default:
      const _exhaustiveCheck: never = hardfork;
      throw new Error(
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we want to print the fork
        `Unknown L1 hardfork '${hardfork as SpecId}', this shouldn't happen`,
      );
  }
}

export function edrOpHardforkToHardhatOpHardforkName(
  hardfork: OpHardfork,
): OpHardforkName {
  switch (hardfork) {
    case OpHardfork.Bedrock:
      return OpHardforkName.BEDROCK;
    case OpHardfork.Regolith:
      return OpHardforkName.REGOLITH;
    case OpHardfork.Canyon:
      return OpHardforkName.CANYON;
    case OpHardfork.Ecotone:
      return OpHardforkName.ECOTONE;
    case OpHardfork.Fjord:
      return OpHardforkName.FJORD;
    case OpHardfork.Granite:
      return OpHardforkName.GRANITE;
    case OpHardfork.Holocene:
      return OpHardforkName.HOLOCENE;
    case OpHardfork.Isthmus:
      return OpHardforkName.ISTHMUS;
    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- trust but verify
    default:
      const _exhaustiveCheck: never = hardfork;
      throw new Error(
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we want to print the fork
        `Unknown OP hardfork '${hardfork as OpHardfork}', this shouldn't happen`,
      );
  }
}

export function hardhatHardforkToEdrSpecId(
  hardfork: string,
  chainType: ChainType,
): string {
  return chainType === OPTIMISM_CHAIN_TYPE
    ? hardhatOpHardforkToEdrSpecId(hardfork)
    : hardhatL1HardforkToEdrSpecId(hardfork);
}

function hardhatOpHardforkToEdrSpecId(hardfork: string): string {
  const hardforkName = getOpHardforkName(hardfork);

  switch (hardforkName) {
    case OpHardforkName.BEDROCK:
      return BEDROCK;
    case OpHardforkName.REGOLITH:
      return REGOLITH;
    case OpHardforkName.CANYON:
      return CANYON;
    case OpHardforkName.ECOTONE:
      return ECOTONE;
    case OpHardforkName.FJORD:
      return FJORD;
    case OpHardforkName.GRANITE:
      return GRANITE;
    case OpHardforkName.HOLOCENE:
      return HOLOCENE;
    case OpHardforkName.ISTHMUS:
      return ISTHMUS;
    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- trust but verify
    default:
      const _exhaustiveCheck: never = hardforkName;
      throw new Error(
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we want to print the fork
        `Unknown hardfork name '${hardforkName as string}', this shouldn't happen`,
      );
  }
}

function hardhatL1HardforkToEdrSpecId(hardfork: string): string {
  const hardforkName = getL1HardforkName(hardfork);

  switch (hardforkName) {
    case L1HardforkName.FRONTIER:
      return FRONTIER;
    case L1HardforkName.HOMESTEAD:
      return HOMESTEAD;
    case L1HardforkName.DAO:
      return DAO_FORK;
    case L1HardforkName.TANGERINE_WHISTLE:
      return TANGERINE;
    case L1HardforkName.SPURIOUS_DRAGON:
      return SPURIOUS_DRAGON;
    case L1HardforkName.BYZANTIUM:
      return BYZANTIUM;
    case L1HardforkName.CONSTANTINOPLE:
      return CONSTANTINOPLE;
    case L1HardforkName.PETERSBURG:
      return PETERSBURG;
    case L1HardforkName.ISTANBUL:
      return ISTANBUL;
    case L1HardforkName.MUIR_GLACIER:
      return MUIR_GLACIER;
    case L1HardforkName.BERLIN:
      return BERLIN;
    case L1HardforkName.LONDON:
      return LONDON;
    case L1HardforkName.ARROW_GLACIER:
      return ARROW_GLACIER;
    case L1HardforkName.GRAY_GLACIER:
      return GRAY_GLACIER;
    case L1HardforkName.MERGE:
      return MERGE;
    case L1HardforkName.SHANGHAI:
      return SHANGHAI;
    case L1HardforkName.CANCUN:
      return CANCUN;
    case L1HardforkName.PRAGUE:
      return PRAGUE;
    case L1HardforkName.OSAKA:
      return OSAKA;

    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- we want to print the fork
    default:
      const _exhaustiveCheck: never = hardforkName;
      throw new Error(
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- an enum can be safely cast to a string
        `Unknown hardfork name '${hardfork as string}', this shouldn't happen`,
      );
  }
}

export function hardhatMiningIntervalToEdrMiningInterval(
  config: EdrNetworkMiningConfig["interval"],
): bigint | IntervalRange | undefined {
  if (typeof config === "number") {
    // Is interval mining disabled?
    if (config === 0) {
      return undefined;
    } else {
      return BigInt(config);
    }
  } else {
    return {
      min: BigInt(config[0]),
      max: BigInt(config[1]),
    };
  }
}

export function hardhatMempoolOrderToEdrMineOrdering(
  mempoolOrder: EdrNetworkMempoolConfig["order"],
): MineOrdering {
  switch (mempoolOrder) {
    case "fifo":
      return MineOrdering.Fifo;
    case "priority":
      return MineOrdering.Priority;
  }
}

export async function hardhatAccountsToEdrOwnedAccounts(
  accounts: EdrNetworkAccountsConfig,
): Promise<Array<{ secretKey: string; balance: bigint }>> {
  const normalizedAccounts = await normalizeEdrNetworkAccountsConfig(accounts);

  const accountPromises = normalizedAccounts.map(async (account) => ({
    secretKey: await account.privateKey.getHexString(),
    balance: account.balance,
  }));

  return Promise.all(accountPromises);
}

export async function normalizeEdrNetworkAccountsConfig(
  accounts: EdrNetworkAccountsConfig,
): Promise<EdrNetworkAccountConfig[]> {
  if (Array.isArray(accounts)) {
    return accounts;
  }

  const isDefaultConfig = await isDefaultEdrNetworkHDAccountsConfig(accounts);
  const derivedPrivateKeys = isDefaultConfig
    ? EDR_NETWORK_DEFAULT_PRIVATE_KEYS
    : await derivePrivateKeys(
        await accounts.mnemonic.get(),
        accounts.path,
        accounts.initialIndex,
        accounts.count,
        await accounts.passphrase.get(),
      );

  return derivedPrivateKeys.map((privateKey) => ({
    privateKey: new FixedValueConfigurationVariable(privateKey),
    balance: accounts.accountsBalance ?? DEFAULT_EDR_NETWORK_BALANCE,
  }));
}

export function hardhatChainDescriptorsToEdrChainOverrides(
  chainDescriptors: ChainDescriptorsConfig,
  chainType: ChainType,
): ChainOverride[] {
  return (
    Array.from(chainDescriptors)
      // Skip chain descriptors that don't match the expected chain type
      .filter(([_, descriptor]) => {
        if (chainType === GENERIC_CHAIN_TYPE) {
          // When "generic" is requested, include both "generic" and "l1" chains
          return (
            descriptor.chainType === GENERIC_CHAIN_TYPE ||
            descriptor.chainType === L1_CHAIN_TYPE
          );
        }

        return descriptor.chainType === chainType;
      })
      .map(([chainId, descriptor]) => {
        const chainOverride: ChainOverride = {
          chainId,
          name: descriptor.name,
        };

        if (descriptor.hardforkHistory !== undefined) {
          chainOverride.hardforkActivationOverrides = Array.from(
            descriptor.hardforkHistory,
          ).map(([hardfork, { blockNumber, timestamp }]) => ({
            condition:
              blockNumber !== undefined
                ? { blockNumber: BigInt(blockNumber) }
                : { timestamp: BigInt(timestamp) },
            hardfork: hardhatHardforkToEdrSpecId(
              hardfork,
              descriptor.chainType,
            ),
          }));
        }

        return chainOverride;
      })
  );
}

export async function hardhatForkingConfigToEdrForkConfig(
  forkingConfig: EdrNetworkForkingConfig | undefined,
  chainDescriptors: ChainDescriptorsConfig,
  chainType: ChainType,
): Promise<ForkConfig | undefined> {
  let fork: ForkConfig | undefined;
  if (forkingConfig !== undefined && forkingConfig.enabled === true) {
    const httpHeaders =
      forkingConfig.httpHeaders !== undefined
        ? Object.entries(forkingConfig.httpHeaders).map(([name, value]) => ({
            name,
            value,
          }))
        : undefined;

    fork = {
      blockNumber: forkingConfig.blockNumber,
      cacheDir: forkingConfig.cacheDir,
      chainOverrides: hardhatChainDescriptorsToEdrChainOverrides(
        chainDescriptors,
        chainType,
      ),
      httpHeaders,
      url: await forkingConfig.url.getUrl(),
    };
  }

  return fork;
}

/**
 * Converts EDR's nested GasReport structure into a flat array of gas entries.
 * Filters out reverted transactions.
 */
export function edrGasReportToHardhatGasMeasurements(
  gasReport: GasReport,
  excludedContractFqns: string[] = [],
): GasMeasurement[] {
  const gasMeasurements: GasMeasurement[] = [];

  for (const [contractFqn, data] of Object.entries(gasReport.contracts)) {
    if (excludedContractFqns.includes(contractFqn)) {
      continue;
    }

    // Process deployments
    for (const deployment of data.deployments) {
      if (deployment.status === GasReportExecutionStatus.Success) {
        gasMeasurements.push({
          contractFqn,
          type: "deployment",
          gas: Number(deployment.gas),
          runtimeSize: Number(deployment.runtimeSize),
        });
      }
    }

    // Process function calls
    for (const [functionSig, calls] of Object.entries(data.functions)) {
      for (const call of calls) {
        if (call.status === GasReportExecutionStatus.Success) {
          gasMeasurements.push({
            contractFqn,
            type: "function",
            functionSig,
            gas: Number(call.gas),
            proxyChain: call.proxyChain,
          });
        }
      }
    }
  }

  return gasMeasurements;
}
