import {AbiId, abis as allAbis} from "../abis/index.js";
import {GmxSdk} from "../index.js";
import {sleep} from "./common.js";

export const MAX_TIMEOUT = 60000; // Increased from 20s to 60s

export type MulticallProviderUrls = {
  primary: string;
  secondary: string;
};

export class Multicall {
  static instances: {
    [chainId: number]: Multicall | undefined;
  } = {};

  static async getInstance(sdk: GmxSdk) {
    const chainId = sdk.chainId;
    let instance = Multicall.instances[chainId];

    if (!instance || instance.chainId !== chainId) {
      instance = new Multicall(sdk);

      Multicall.instances[chainId] = instance;
    }

    return instance;
  }

  constructor(public sdk: GmxSdk) {}

  get chainId() {
    return this.sdk.chainId;
  }

  async call(request: MulticallRequestConfig<any>, maxTimeout: number, retryCount: number = 0) {
    const client = this.sdk.publicClient;

    if (!client) {
      throw new Error("Public client is not initialized");
    }

    const MAX_RETRIES = 3;
    const BASE_DELAY = 1000; // 1 second base delay

    try {
      return await this.executeMulticallWithRetry(request, maxTimeout, retryCount, MAX_RETRIES, BASE_DELAY);
    } catch (error) {
      // If all retries failed, throw the error
      throw error;
    }
  }

  private async executeMulticallWithRetry(
    request: MulticallRequestConfig<any>, 
    maxTimeout: number, 
    retryCount: number, 
    maxRetries: number, 
    baseDelay: number
  ) {
    const client = this.sdk.publicClient;

    const originalKeys: {
      contractKey: string;
      callKey: string;
    }[] = [];

    const abis: any = {};

    const encodedPayload: { address: `0x${string}`; abi: any; functionName: string; args: any }[] = [];

    const contractKeys = Object.keys(request);

    contractKeys.forEach((contractKey) => {
      const contractCallConfig = request[contractKey];

      if (!contractCallConfig) {
        return;
      }

      Object.keys(contractCallConfig.calls).forEach((callKey) => {
        const call = contractCallConfig.calls[callKey];

        if (!call) {
          return;
        }

        // Add Errors ABI to each contract ABI to correctly parse errors
        abis[contractCallConfig.contractAddress] = abis[contractCallConfig.contractAddress] || [
          ...allAbis[contractCallConfig.abiId],
          ...allAbis.CustomErrors,
        ];
        const abi = abis[contractCallConfig.contractAddress];

        originalKeys.push({
          contractKey,
          callKey,
        });

        encodedPayload.push({
          address: contractCallConfig.contractAddress as `0x${string}`,
          functionName: call.methodName,
          args: call.params,
          abi,
        });
      });
    });

    const processResponse = (response: any) => {
      const multicallResult: MulticallResult<any> = {
        success: true,
        errors: {},
        data: {},
      };

      response.forEach(({ result, status, error }: { result: any; status: string; error: any }, i: number) => {
        const { contractKey, callKey } = originalKeys[i];

        if (status === "success") {
          let values: any;

          if (Array.isArray(result) || typeof result === "object") {
            values = result;
          } else {
            values = [result];
          }

          multicallResult.data[contractKey] = multicallResult.data[contractKey] || {};
          multicallResult.data[contractKey][callKey] = {
            contractKey,
            callKey,
            returnValues: values,
            success: true,
          };
        } else {
          multicallResult.success = false;

          multicallResult.errors[contractKey] = multicallResult.errors[contractKey] || {};
          multicallResult.errors[contractKey][callKey] = error;

          multicallResult.data[contractKey] = multicallResult.data[contractKey] || {};
          multicallResult.data[contractKey][callKey] = {
            contractKey,
            callKey,
            returnValues: [],
            success: false,
            error: error,
          };
        }
      });

      return multicallResult;
    };

    const timeoutController = new AbortController();

    const multicallPromise = (client.multicall as any)({ contracts: encodedPayload });
    const timeoutPromise = sleep(maxTimeout, timeoutController.signal).then(() => Promise.reject(new Error("multicall timeout")));
    
    try {
      const result = await Promise.race([multicallPromise, timeoutPromise])
        .then((response) => {
          timeoutController.abort();
          return processResponse(response);
        });

      if (result.success) {
        return result;
      }

      /* eslint-disable-next-line */
      console.error(result.errors);

      return result;
    } catch (error) {
      timeoutController.abort();
      
      // Better error handling to distinguish between different types of errors
      let errorMessage = error.message || 'Unknown multicall error';
      let shouldRetry = false;
      
      if (errorMessage.includes('timeout') || errorMessage.includes('TIMEOUT')) {
        errorMessage = 'multicall timeout';
        shouldRetry = true;
      } else if (errorMessage.includes('RPC') || errorMessage.includes('rpc')) {
        errorMessage = 'RPC error: ' + errorMessage;
        shouldRetry = true;
      } else if (errorMessage.includes('network') || errorMessage.includes('NETWORK')) {
        errorMessage = 'Network error: ' + errorMessage;
        shouldRetry = true;
      } else if (errorMessage.includes('rate limit') || errorMessage.includes('RATE_LIMIT')) {
        errorMessage = 'Rate limit error: ' + errorMessage;
        shouldRetry = true;
      } else {
        errorMessage = 'Multicall error: ' + errorMessage.slice(0, 150);
        shouldRetry = false; // Don't retry for other types of errors
      }

      // If we should retry and haven't exceeded max retries, retry with exponential backoff
      if (shouldRetry && retryCount < maxRetries) {
        const delay = baseDelay * Math.pow(2, retryCount); // Exponential backoff
        console.log(`Multicall failed (attempt ${retryCount + 1}/${maxRetries + 1}), retrying in ${delay}ms: ${errorMessage}`);
        
        await sleep(delay);
        return this.executeMulticallWithRetry(request, maxTimeout, retryCount + 1, maxRetries, baseDelay);
      }

      const e = new Error(errorMessage);
      e.name = 'MulticallError';

      /* eslint-disable-next-line */
      console.error('Multicall failed after all retries:', e.message);

      throw e;
    }
  }
}

export type SkipKey = null | undefined | false;

export type ContractCallConfig = {
  methodName: string;
  params: any[];
};

export type ContractCallsConfig<T extends { calls: any }> = {
  contractAddress: string;
  abiId: AbiId;
  calls: {
    [callKey in keyof T["calls"]]: ContractCallConfig | SkipKey;
  };
};

export type MulticallRequestConfig<T extends { [key: string]: any }> = {
  [contractKey in keyof T]: ContractCallsConfig<T[contractKey]>;
};

export type ContractCallResult = {
  returnValues: {
    [key: string | number]: any;
  };
  contractKey: string;
  callKey: string;
  success?: boolean;
  error?: string;
};

export type MulticallErrors<T extends MulticallRequestConfig<any>> = {
  [contractKey in keyof T]: {
    [callKey in keyof T[contractKey]["calls"]]: {
      message: string;
      shortMessage: string;
      functionName: string;
      contractAddress: string;
    };
  };
};

export type ContractCallsResult<T extends ContractCallsConfig<any>> = {
  [callKey in keyof T["calls"]]: ContractCallResult;
};

export type MulticallResult<T extends MulticallRequestConfig<any>> = {
  success: boolean;
  errors: MulticallErrors<T>;
  data: {
    [contractKey in keyof T]: ContractCallsResult<T[contractKey]>;
  };
};
