// Copyright 2017-2025 @pezkuwi/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Observable } from 'rxjs';
import type { Balance } from '@pezkuwi/types/interfaces';
import type { PezpalletStakingStakingLedger, PezpalletStakingUnlockChunk } from '@pezkuwi/types/lookup';
import type { DeriveApi, DeriveSessionInfo, DeriveStakingAccount, DeriveStakingKeys, DeriveStakingQuery, DeriveUnlocking } from '../types.js';
import type { StakingQueryFlags } from './types.js';

import { combineLatest, map, switchMap } from 'rxjs';

import { BN, BN_ZERO, objectSpread } from '@pezkuwi/util';

import { firstMemo, memo } from '../util/index.js';

const QUERY_OPTS = {
  withDestination: true,
  withLedger: true,
  withNominations: true,
  withPrefs: true
};

function groupByEra (list: PezpalletStakingUnlockChunk[]): Record<string, BN> {
  return list.reduce((map: Record<string, BN>, { era, value }): Record<string, BN> => {
    const key = era.toString();

    map[key] = (map[key] || BN_ZERO).add(value.unwrap());

    return map;
  }, {});
}

function calculateUnlocking (api: DeriveApi, stakingLedger: PezpalletStakingStakingLedger | undefined, sessionInfo: DeriveSessionInfo): DeriveUnlocking[] | undefined {
  const results = Object
    .entries(groupByEra(
      (stakingLedger?.unlocking || []).filter(({ era }) => era.unwrap().gt(sessionInfo.activeEra))
    ))
    .map(([eraString, value]): DeriveUnlocking => ({
      remainingEras: new BN(eraString).isub(sessionInfo.activeEra),
      value: api.registry.createType('Balance', value)
    }));

  return results.length
    ? results
    : undefined;
}

function redeemableSum (api: DeriveApi, stakingLedger: PezpalletStakingStakingLedger | undefined, sessionInfo: DeriveSessionInfo): Balance {
  return api.registry.createType('Balance', (stakingLedger?.unlocking || [] as PezpalletStakingUnlockChunk[]).reduce((total, { era, value }): BN => {
    // aligns with https://github.com/pezkuwichain/bizinikiwi/blob/fdfdc73f9e64dc47934b72eb9af3e1989e4ba699/frame/staking/src/pallet/mod.rs#L973-L975
    // (ensure currentEra >= era passed, as per https://github.com/pezkuwichain/bizinikiwi/blob/fdfdc73f9e64dc47934b72eb9af3e1989e4ba699/frame/staking/src/lib.rs#L477-L494)
    // NOTE: Previously we used activeEra >= era, which is incorrect for the last session
    return era.unwrap().gt(sessionInfo.currentEra)
      ? total
      : total.iadd(value.unwrap());
  }, new BN(0)));
}

function parseResult (api: DeriveApi, sessionInfo: DeriveSessionInfo, keys: DeriveStakingKeys, query: DeriveStakingQuery): DeriveStakingAccount {
  return objectSpread({}, keys, query, {
    redeemable: redeemableSum(api, query.stakingLedger, sessionInfo),
    unlocking: calculateUnlocking(api, query.stakingLedger, sessionInfo)
  });
}

/**
 * @name accounts
 * @param {(Uint8Array | string)[]} accountIds List of account stashes
 * @param {StakingQueryFlags} opts optional filtering flag
 * @description From a list of stashes, fill in all the relevant staking details
 * @example
 * ```javascript
 * const accounts = await api.derive.staking.accounts([
 *  "149B17nn7zVL4SkLSNmANupEkGexUBAxVrdk4bbWFZYibkFc",
 * ]);
 * console.log("First account staking info:", accounts[0]);
 * ```
 */
export function accounts (instanceId: string, api: DeriveApi): (accountIds: (Uint8Array | string)[], opts?: StakingQueryFlags) => Observable<DeriveStakingAccount[]> {
  return memo(instanceId, (accountIds: (Uint8Array | string)[], opts: StakingQueryFlags = QUERY_OPTS): Observable<DeriveStakingAccount[]> =>
    api.derive.session.info().pipe(
      switchMap((sessionInfo) =>
        combineLatest([
          api.derive.staking.keysMulti(accountIds),
          api.derive.staking.queryMulti(accountIds, opts)
        ]).pipe(
          map(([keys, queries]) =>
            queries.map((q, index) => parseResult(api, sessionInfo, keys[index], q))
          )
        )
      )
    )
  );
}

/**
 * @name account
 * @param {(Uint8Array | string)} accountId AccountId of the stash.
 * @param {StakingQueryFlags} opts (Optional) filtering flag.
 * @description From a stash, retrieve the controllerId and fill in all the relevant staking details.
 * @example
 * ```javascript
 * const accountStakingData = await api.derive.staking.account(
 *   "149B17nn7zVL4SkLSNmANupEkGexUBAxVrdk4bbWFZYibkFc"
 * );
 * console.log(accountStakingData);
 * ```
 */
export const account = /*#__PURE__*/ firstMemo(
  (api: DeriveApi, accountId: Uint8Array | string, opts?: StakingQueryFlags) =>
    api.derive.staking.accounts([accountId], opts)
);
