import type { Address, BlockHash, BlockCountRPC, BlockInfoRPC, BlocksRPC, BlocksInfoRPC, RepresentativesRPC, RepresentativesOnlineRPC, RepresentativesOnlineWeightRPC, AccountHistoryRPC, AccountHistoryRawRPC, AccountInfoRPC, AccountBalanceRPC, AccountsBalancesRPC, AccountRepresentativeRPC, AccountsRepresentativesRPC, AccountWeightRPC, AccountReceivableRPC, AccountReceivableThresholdRPC, AccountReceivableSourceRPC, DelegatorsRPC, DelegatorsCountRPC, TelemetryRPC, TelemetryRawRPC, TelemetryAddressRPC, VersionRPC } from "./rpc_types";

/** Implement this interface if the built-in RPC class does not fit your needs. The easiest way to do this is by just extending the built-in RPC class */
export interface RPCInterface {
  rpc_url: string;
  use_pending: boolean;
  DECIMALS?: number;
  call(payload: Record<string, any>): Promise<Record<string, string>>;
  get_block_info(block_hash: BlockHash): Promise<BlockInfoRPC>;
  get_account_info(account: Address, include_confirmed?: boolean, representative?: boolean, weight?: boolean, pending?: boolean): Promise<AccountInfoRPC>;
  get_account_receivable(account: Address, count?: number, threshold?: `${number}`, source?: boolean): Promise<AccountReceivableRPC | AccountReceivableThresholdRPC | AccountReceivableSourceRPC>;
}

/** Sends RPC requests to the RPC node, also has wrappers for actions that only read the network (write actions are handled by the Wallet class) */
export class RPC implements RPCInterface {
  rpc_url: string;

  use_pending: boolean;
  DECIMALS = undefined; //for nano, change to nano decimals

  debug: boolean = false;

  /** HTTP headers to send with any RPC requests, defaults to { "Content-Type": "application/json" } */
  headers: Record<string, string> | undefined;

  /**
   * @param {boolean} [use_pending = false] If true, uses "pending" instead of "receivable" in RPC action names, for compatibility with older versions of the node
   */
  constructor(rpc_url: string, use_pending: boolean = false) {
    this.rpc_url = rpc_url;
    this.use_pending = use_pending;
  }

  //Network information related

  /** The function that sends the RPC POST request */
  async call<T extends Record<string, any>>(payload: Record<string, any>): Promise<T> {
    if (this.debug) console.log(JSON.stringify(payload));
    const resp = await fetch(this.rpc_url, {
      method: "POST",
      headers: this.headers ?? { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (!resp.ok && this.debug) console.log(await resp.text());
    if (!resp.ok) throw Error(`Request to RPC node failed with status code ${resp.status}`);
    const resp_json = await resp.json();
    if (resp_json.error) throw Error(`RPC node response: ${resp_json.error}`);
    return resp_json;
  }
  /** See https://docs.nano.org/commands/rpc-protocol/#block_count */
  async get_block_count(): Promise<BlockCountRPC> {
    return (await this.call({
      action: "block_count",
    })) as BlockCountRPC;
  }
  /** See https://docs.nano.org/commands/rpc-protocol/#block_info */
  async get_block_info(block_hash: BlockHash): Promise<BlockInfoRPC> {
    return (await this.call({
      action: "block_info",
      hash: block_hash,
      json_block: true,
    })) as BlockInfoRPC;
  }
  /** See https://docs.nano.org/commands/rpc-protocol/#blocks */
  async get_blocks(block_hashes: BlockHash[]): Promise<BlocksRPC> {
    return (await this.call({
      action: "blocks",
      hashes: block_hashes,
      json_block: true,
    })) as BlocksRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#blocks_info */
  async get_blocks_info(block_hashes: BlockHash[]): Promise<BlocksInfoRPC> {
    return (await this.call({
      action: "blocks_info",
      hashes: block_hashes,
      json_block: true,
    })) as BlocksInfoRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#representatives */
  async get_representatives(): Promise<RepresentativesRPC> {
    return (await this.call({
      action: "representatives",
    })) as RepresentativesRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#representatives_online */
  async get_representatives_online<T extends boolean>(weight?: T): Promise<T extends true ? RepresentativesOnlineWeightRPC : RepresentativesOnlineRPC> {
    return (await this.call({
      action: "representatives_online",
      weight: weight ? true : undefined, //better not to include "weight" if false, rather than sending "weight": false
    })) as T extends true ? RepresentativesOnlineWeightRPC : RepresentativesOnlineRPC;
  }

  //Account information related

  /** https://docs.nano.org/commands/rpc-protocol/#account_history */
  async get_account_history<T extends boolean>(account: Address, count: number, raw?: T, head?: BlockHash, offset?: number, reverse?: boolean, account_filter?: Address[]): Promise<T extends true ? AccountHistoryRawRPC : AccountHistoryRPC> {
    return (await this.call({
      action: "account_history",
      account,
      count: `${count}`,
      raw: raw ? true : undefined,
      head,
      offset: offset ? `${offset}` : undefined,
      reverse: reverse ? true : undefined,
      account_filter,
    })) as T extends true ? AccountHistoryRawRPC : AccountHistoryRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#account_info */
  async get_account_info(account: Address, include_confirmed?: boolean, representative?: boolean, weight?: boolean, pending?: boolean): Promise<AccountInfoRPC> {
    return (await this.call({
      action: "account_info",
      account,
      include_confirmed: include_confirmed ? true : undefined,
      representative: representative ? true : undefined,
      weight: weight ? true : undefined,
      pending: pending ? true : undefined,
    })) as AccountInfoRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#account_balance */
  async get_account_balance(account: Address): Promise<AccountBalanceRPC> {
    return (await this.call({
      action: "account_balance",
      account,
    })) as AccountBalanceRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#accounts_balances */
  async get_accounts_balances(accounts: Address[]): Promise<AccountsBalancesRPC> {
    return (await this.call({
      action: "accounts_balances",
      accounts,
    })) as AccountsBalancesRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#account_representative */
  async get_account_representative(account: Address): Promise<AccountRepresentativeRPC> {
    return (await this.call({
      action: "account_representative",
      account,
    })) as AccountRepresentativeRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#accounts_representatives */
  async get_accounts_representatives(account: Address): Promise<AccountsRepresentativesRPC> {
    return (await this.call({
      action: "accounts_representatives",
      account,
    })) as AccountsRepresentativesRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#account_weight */
  async get_account_weight(account: Address): Promise<AccountWeightRPC> {
    return (await this.call({
      action: "account_weight",
      account,
    })) as AccountWeightRPC;
  }
  //I hate nano node rpc here. I don't want to do the conditional type thing here, so have a union
  /** Keep in mind "threshold" parameter is in raw. https://docs.nano.org/commands/rpc-protocol/#receivable */
  async get_account_receivable(account: Address, count?: number, threshold?: `${number}`, source?: boolean): Promise<AccountReceivableRPC | AccountReceivableThresholdRPC | AccountReceivableSourceRPC> {
    return (await this.call({
      action: this.use_pending ? "pending" : "receivable",
      account,
      count: count ? `${count}` : undefined,
      threshold,
      source: source ? true : undefined,
    })) as AccountReceivableRPC | AccountReceivableThresholdRPC | AccountReceivableSourceRPC;
  }
  /** Keep in mind "threshold" parameter is in raw. https://docs.nano.org/commands/rpc-protocol/#delegators */
  async get_delegators(account: Address, threshold?: `${number}`, count?: number, start?: Address): Promise<DelegatorsRPC> {
    return (await this.call({
      action: "delegators",
      account,
      threshold,
      count: count ? `${count}` : undefined,
      start,
    })) as DelegatorsRPC;
  }
  /** https://docs.nano.org/commands/rpc-protocol/#account_weight */
  async get_delegators_count(account: Address): Promise<DelegatorsCountRPC> {
    return (await this.call({
      action: "account_weight",
      account,
    })) as DelegatorsCountRPC;
  }

  /** https://docs.nano.org/commands/rpc-protocol/#telemetry */
  async get_telemetry(raw?: boolean, address?: string, port?: number): Promise<TelemetryRPC | { metrics: TelemetryRawRPC[] } | TelemetryAddressRPC> {
    return (await this.call({
      action: "telemetry",
      raw: raw ? raw : undefined,
      address: address ? address : undefined,
      port: port ? `${port}` : undefined,
    })) as TelemetryRPC | { metrics: TelemetryRawRPC[] } | TelemetryAddressRPC;
  }

  /** https://docs.nano.org/commands/rpc-protocol/#version */
  async get_version(): Promise<VersionRPC> {
    return (await this.call({
      action: "version",
    })) as VersionRPC;
  }
}

export class RPCWithBackup extends RPC {
  readonly rpc_urls: string[];
  readonly timeout: number;

  /**
   * @param {number} [timeout] Request to RPC timeout, in milliseconds. If RPC request fails or timeouts, tries the next RPC
   */
  constructor(rpc_urls: string[], timeout: number, use_pending: boolean = false) {
    if (rpc_urls.length < 2) throw Error("Must provide at least two RPC URLs");
    super(rpc_urls[0], use_pending);
    this.rpc_urls = rpc_urls;
    this.timeout = timeout;
  }
  async call<T extends Record<string, any>>(payload: Record<string, any>): Promise<T> {
    let i = 0;
    while (true) {
      try {
        const resp = await fetch(this.rpc_urls[i], {
          method: "POST",
          headers: this.headers ?? { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
          signal: (AbortSignal as any).timeout(this.timeout), //for old typescript versions
        });
        if (!resp.ok) throw Error(`Request to RPC node failed with status code ${resp.status}`);
        const resp_json = await resp.json();
        if (resp_json.error) throw Error(`RPC node response: ${resp_json.error}`);
        return resp_json;
      } catch (e) {
        //increment (so try next RPC in provided list), if all RPCs exhausted (all failed), throw error
        //typescript says e might not inherit from Error which is technically true, but in this case it always will be
        if (!this.rpc_urls[++i]) throw Error(e instanceof Error ? e.toString() : "RPC call error");
      }
    }
  }
}
