// ========================================
// Staking Helper Functions
// ========================================
// Helper functions for pallet_staking and pallet_staking_score integration

import { ApiPromise } from '@pezkuwi/api';
import { formatBalance } from './wallet';

export interface StakingLedger {
  stash: string;
  total: string;
  active: string;
  unlocking: { value: string; era: number }[];
  claimedRewards: number[];
}

export interface NominatorInfo {
  targets: string[];
  submittedIn: number;
  suppressed: boolean;
}

export interface ValidatorPrefs {
  commission: number;
  blocked: boolean;
}

export interface EraRewardPoints {
  total: number;
  individual: Record<string, number>;
}

export interface PezRewardInfo {
  currentEpoch: number;
  epochStartBlock: number;
  claimableRewards: { epoch: number; amount: string }[]; // Unclaimed rewards from completed epochs
  totalClaimable: string;
  hasPendingClaim: boolean;
}

export interface StakingInfo {
  bonded: string;
  active: string;
  unlocking: { amount: string; era: number; blocksRemaining: number }[];
  redeemable: string;
  nominations: string[];
  stakingScore: number | null;
  stakingDuration: number | null; // Duration in blocks
  hasStartedScoreTracking: boolean;
  isValidator: boolean;
  pezRewards: PezRewardInfo | null; // PEZ rewards information
}

/**
 * Get staking ledger for an account
 * In Substrate staking, we need to query using the controller account.
 * If stash == controller (modern setup), we can query directly.
 */
export async function getStakingLedger(
  api: ApiPromise,
  address: string
): Promise<StakingLedger | null> {
  try {
    // Method 1: Try direct ledger query (modern Substrate where stash == controller)
    let ledgerResult = await api.query.staking.ledger(address);

    // Method 2: If not found, check if address is a stash and get controller
    if (ledgerResult.isNone) {
      const bondedController = await api.query.staking.bonded(address);
      if (bondedController.isSome) {
        const controllerCodec = bondedController.unwrap() as { toString: () => string };
        const controllerAddress = controllerCodec.toString();
        console.log(`Found controller ${controllerAddress} for stash ${address}`);
        ledgerResult = await api.query.staking.ledger(controllerAddress);
      }
    }

    if (ledgerResult.isNone) {
      console.warn(`No staking ledger found for ${address}`);
      return null;
    }

    const ledger = ledgerResult.unwrap() as { toJSON: () => unknown };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const ledgerJson = ledger.toJSON() as any;

    console.log('Staking ledger:', ledgerJson);

    return {
      stash: ledgerJson.stash?.toString() || address,
      total: ledgerJson.total?.toString() || '0',
      active: ledgerJson.active?.toString() || '0',
      unlocking: (ledgerJson.unlocking || []).map((u: any) => ({
        value: u.value?.toString() || '0',
        era: u.era || 0
      })),
      claimedRewards: ledgerJson.claimedRewards || []
    };
  } catch (error) {
    console.error('Error fetching staking ledger:', error);
    return null;
  }
}

/**
 * Get nominations for an account
 */
export async function getNominations(
  api: ApiPromise,
  address: string
): Promise<NominatorInfo | null> {
  try {
    const nominatorsOption = await api.query.staking.nominators(address);

    if (nominatorsOption.isNone) {
      return null;
    }

    const nominator = nominatorsOption.unwrap() as { toJSON: () => unknown };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const nominatorJson = nominator.toJSON() as any;

    return {
      targets: nominatorJson.targets || [],
      submittedIn: nominatorJson.submittedIn || 0,
      suppressed: nominatorJson.suppressed || false
    };
  } catch (error) {
    console.error('Error fetching nominations:', error);
    return null;
  }
}

/**
 * Get current active era
 */
export async function getCurrentEra(api: ApiPromise): Promise<number> {
  try {
    const activeEraOption = await api.query.staking.activeEra();
    if (activeEraOption.isNone) {
      return 0;
    }
    const activeEra = activeEraOption.unwrap() as { index: { toString: () => string } };
    return Number(activeEra.index.toString());
  } catch (error) {
    console.error('Error fetching current era:', error);
    return 0;
  }
}

/**
 * Get blocks remaining until an era
 */
export async function getBlocksUntilEra(
  api: ApiPromise,
  targetEra: number
): Promise<number> {
  try {
    const currentEra = await getCurrentEra(api);
    if (targetEra <= currentEra) {
      return 0;
    }

    const activeEraOption = await api.query.staking.activeEra();
    if (activeEraOption.isNone) {
      return 0;
    }

    const activeEra = activeEraOption.unwrap() as { start: { unwrapOr: (def: number) => { toString: () => string } } };
    const eraStartBlock = Number(activeEra.start.unwrapOr(0).toString());

    // Get session length and sessions per era
    const sessionLength = api.consts.babe?.epochDuration || api.consts.timestamp?.minimumPeriod || 600;
    const sessionsPerEra = api.consts.staking.sessionsPerEra;

    const blocksPerEra = Number(sessionLength.toString()) * Number(sessionsPerEra.toString());
    const currentBlock = Number((await api.query.system.number()).toString());

    const erasRemaining = targetEra - currentEra;
    const blocksIntoCurrentEra = currentBlock - eraStartBlock;
    const blocksRemainingInCurrentEra = blocksPerEra - blocksIntoCurrentEra;

    return blocksRemainingInCurrentEra + (blocksPerEra * (erasRemaining - 1));
  } catch (error) {
    console.error('Error calculating blocks until era:', error);
    return 0;
  }
}

/**
 * Get PEZ rewards information for an account
 * Note: pezRewards pallet is on People Chain, not Relay Chain
 */
export async function getPezRewards(
  peopleApi: ApiPromise,
  address: string
): Promise<PezRewardInfo | null> {
  try {
    // Check if pezRewards pallet exists on People Chain
    if (!peopleApi?.query?.pezRewards || !peopleApi.query.pezRewards.epochInfo) {
      console.warn('PezRewards pallet not available on People Chain');
      return null;
    }

    // Get current epoch info
    const epochInfoResult = await peopleApi.query.pezRewards.epochInfo();

    if (!epochInfoResult) {
      console.warn('No epoch info found');
      return null;
    }

    const epochInfo = epochInfoResult.toJSON() as any;
    const currentEpoch = epochInfo.currentEpoch || 0;
    const epochStartBlock = epochInfo.epochStartBlock || 0;

    // Check for claimable rewards from completed epochs
    const claimableRewards: { epoch: number; amount: string }[] = [];
    let totalClaimable = BigInt(0);

    // Check last 3 completed epochs for unclaimed rewards
    for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
      try {
        // Check if user has claimed this epoch already
        const claimedResult = await peopleApi.query.pezRewards.claimedRewards(i, address);

        if (claimedResult.isNone) {
          // User hasn't claimed - check if they have rewards
          const userScoreResult = await peopleApi.query.pezRewards.userEpochScores(i, address);

          if (userScoreResult.isSome) {
            // User has a score for this epoch - calculate their reward
            const epochPoolResult = await peopleApi.query.pezRewards.epochRewardPools(i);

            if (epochPoolResult.isSome) {
              const epochPoolCodec = epochPoolResult.unwrap() as { toJSON: () => unknown };
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              const epochPool = epochPoolCodec.toJSON() as any;
              const userScoreCodec = userScoreResult.unwrap() as { toString: () => string };
              const userScore = BigInt(userScoreCodec.toString());
              const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint || '0');

              const rewardAmount = userScore * rewardPerPoint;
              const rewardFormatted = formatBalance(rewardAmount.toString());

              if (parseFloat(rewardFormatted) > 0) {
                claimableRewards.push({
                  epoch: i,
                  amount: rewardFormatted
                });
                totalClaimable += rewardAmount;
              }
            }
          }
        }
      } catch (err) {
        console.warn(`Error checking epoch ${i} rewards:`, err);
      }
    }

    return {
      currentEpoch,
      epochStartBlock,
      claimableRewards,
      totalClaimable: formatBalance(totalClaimable.toString()),
      hasPendingClaim: claimableRewards.length > 0
    };
  } catch (error) {
    console.warn('PEZ rewards not available:', error);
    return null;
  }
}

/**
 * Get comprehensive staking info for an account
 * @param api - Relay Chain API (for staking pallet)
 * @param address - User address
 * @param peopleApi - Optional People Chain API (for pezRewards and stakingScore pallets)
 */
export async function getStakingInfo(
  api: ApiPromise,
  address: string,
  peopleApi?: ApiPromise
): Promise<StakingInfo> {
  const ledger = await getStakingLedger(api, address);
  const nominations = await getNominations(api, address);
  const currentEra = await getCurrentEra(api);

  const unlocking = ledger?.unlocking || [];
  const unlockingWithBlocks = await Promise.all(
    unlocking.map(async (u) => {
      const blocks = await getBlocksUntilEra(api, u.era);
      return {
        amount: formatBalance(u.value),
        era: u.era,
        blocksRemaining: blocks
      };
    })
  );

  // Calculate redeemable (unlocking chunks where era has passed)
  const redeemableChunks = unlocking.filter(u => u.era <= currentEra);
  const redeemable = redeemableChunks.reduce((sum, u) => {
    return sum + BigInt(u.value);
  }, BigInt(0));

  // Get staking score if available
  //
  // IMPORTANT: This calculation MUST match pallet_staking_score::get_staking_score() exactly!
  // The pallet calculates this score and reports it to pallet_pez_rewards.
  // Any changes here must be synchronized with pallets/staking-score/src/lib.rs
  //
  // Score Formula:
  // 1. Amount Score (20-50 points based on staked HEZ)
  //    - 0-100 HEZ: 20 points
  //    - 101-250 HEZ: 30 points
  //    - 251-750 HEZ: 40 points
  //    - 751+ HEZ: 50 points
  // 2. Duration Multiplier (based on time staked)
  //    - < 1 month: x1.0
  //    - 1-2 months: x1.2
  //    - 3-5 months: x1.4
  //    - 6-11 months: x1.7
  //    - 12+ months: x2.0
  // 3. Final Score = min(100, floor(amountScore * durationMultiplier))
  //
  let stakingScore: number | null = null;
  let stakingDuration: number | null = null;
  let hasStartedScoreTracking = false;

  try {
    // stakingScore pallet is on People Chain
    const scoreApi = peopleApi || api;
    if (scoreApi.query.stakingScore && scoreApi.query.stakingScore.stakingStartBlock) {
      // Check if user has started score tracking
      const scoreResult = await scoreApi.query.stakingScore.stakingStartBlock(address);

      if (scoreResult.isSome) {
        hasStartedScoreTracking = true;
        const startBlockCodec = scoreResult.unwrap() as { toString: () => string };
        const startBlock = Number(startBlockCodec.toString());
        const currentBlock = Number((await scoreApi.query.system.number()).toString());
        const durationInBlocks = currentBlock - startBlock;
        stakingDuration = durationInBlocks;

        // Calculate amount-based score (20-50 points)
        const stakedHEZ = ledger ? parseFloat(formatBalance(ledger.total)) : 0;
        let amountScore = 20; // Default

        if (stakedHEZ <= 100) {
          amountScore = 20;
        } else if (stakedHEZ <= 250) {
          amountScore = 30;
        } else if (stakedHEZ <= 750) {
          amountScore = 40;
        } else {
          amountScore = 50; // 751+ HEZ
        }

        // Calculate duration multiplier
        const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // 432,000 blocks (~30 days, 6s per block)
        let durationMultiplier = 1.0;

        if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) {
          durationMultiplier = 2.0; // 12+ months
        } else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) {
          durationMultiplier = 1.7; // 6-11 months
        } else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) {
          durationMultiplier = 1.4; // 3-5 months
        } else if (durationInBlocks >= MONTH_IN_BLOCKS) {
          durationMultiplier = 1.2; // 1-2 months
        } else {
          durationMultiplier = 1.0; // < 1 month
        }

        // Final score calculation (max 100)
        // This MUST match the pallet's integer math: amount_score * multiplier_numerator / multiplier_denominator
        stakingScore = Math.min(100, Math.floor(amountScore * durationMultiplier));

        console.log('Staking score calculated:', {
          stakedHEZ,
          amountScore,
          durationInBlocks,
          durationMultiplier,
          finalScore: stakingScore
        });
      }
    }
  } catch (error) {
    console.warn('Staking score not available:', error);
  }

  // Check if validator
  const validatorsOption = await api.query.staking.validators(address);
  const isValidator = validatorsOption.isSome;

  // Get PEZ rewards information (from People Chain)
  const pezRewards = peopleApi ? await getPezRewards(peopleApi, address) : null;

  return {
    bonded: ledger ? formatBalance(ledger.total) : '0',
    active: ledger ? formatBalance(ledger.active) : '0',
    unlocking: unlockingWithBlocks,
    redeemable: formatBalance(redeemable.toString()),
    nominations: nominations?.targets || [],
    stakingScore,
    stakingDuration,
    hasStartedScoreTracking,
    isValidator,
    pezRewards
  };
}

/**
 * Get list of active validators
 * For Pezkuwi, we query staking.validators.entries() to get all registered validators
 */
export async function getActiveValidators(api: ApiPromise): Promise<string[]> {
  try {
    // Try multiple methods to get validators

    // Method 1: Try validatorPool.currentValidatorSet() if available
    if (api.query.validatorPool && api.query.validatorPool.currentValidatorSet) {
      try {
        const currentSetOption = await api.query.validatorPool.currentValidatorSet();
        if (currentSetOption.isSome) {
          const validatorSet = currentSetOption.unwrap() as any;
          // Extract validators array from the set structure
          if (validatorSet.validators && Array.isArray(validatorSet.validators)) {
            const validators = validatorSet.validators.map((v: any) => v.toString());
            if (validators.length > 0) {
              console.log(`Found ${validators.length} validators from validatorPool.currentValidatorSet`);
              return validators;
            }
          }
        }
      } catch (err) {
        console.warn('validatorPool.currentValidatorSet query failed:', err);
      }
    }

    // Method 2: Query staking.validators.entries() to get all registered validators
    try {
      const validatorEntries = await api.query.staking.validators.entries();
      if (validatorEntries.length > 0) {
        const validators = validatorEntries.map(([key]) => key.args[0].toString());
        console.log(`Found ${validators.length} validators from staking.validators.entries()`);
        return validators;
      }
    } catch (err) {
      console.warn('staking.validators.entries() query failed:', err);
    }

    // Method 3: Fallback to session.validators()
    const sessionValidators = await api.query.session.validators();
    const validatorArray = Array.isArray(sessionValidators)
      ? sessionValidators
      : (sessionValidators as unknown as { toJSON: () => string[] }).toJSON();
    const validators = validatorArray.map((v: unknown) => String(v));
    console.log(`Found ${validators.length} validators from session.validators()`);
    return validators;
  } catch (error) {
    console.error('Error fetching validators:', error);
    return [];
  }
}

/**
 * Get minimum nominator bond
 */
export async function getMinNominatorBond(api: ApiPromise): Promise<string> {
  try {
    const minBond = await api.query.staking.minNominatorBond();
    return formatBalance(minBond.toString());
  } catch (error) {
    console.error('Error fetching min nominator bond:', error);
    return '0';
  }
}

/**
 * Get bonding duration in eras
 */
export async function getBondingDuration(api: ApiPromise): Promise<number> {
  try {
    const duration = api.consts.staking.bondingDuration;
    return Number(duration.toString());
  } catch (error) {
    console.error('Error fetching bonding duration:', error);
    return 28; // Default 28 eras
  }
}

/**
 * Parse amount to blockchain format (12 decimals for HEZ)
 */
export function parseAmount(amount: string | number, decimals: number = 12): string {
  const amountNum = typeof amount === 'string' ? parseFloat(amount) : amount;
  if (isNaN(amountNum) || amountNum <= 0) {
    throw new Error('Invalid amount');
  }
  const value = BigInt(Math.floor(amountNum * Math.pow(10, decimals)));
  return value.toString();
}
