const ChainBlock = require('../models/ChainBlock');
const ValidatorVote = require('../models/ValidatorVote');
const callbackService = require('./callbackService');
const nodeParticipantService = require('./nodeParticipantService');
const persistentStore = require('./persistentStore');

const state = persistentStore.readState();
const blocks = state.blocks;
const validatorVotes = state.validatorVotes;

function getAutoMineSize() {
  return Math.max(1, Number(process.env.AISFOR_CHAIN_AUTO_MINE_SIZE || 3));
}

function getValidatorRewardPoint() {
  return Math.max(0, Number(process.env.AISFOR_CHAIN_VALIDATOR_REWARD || 2));
}

function getMinValidatorStake() {
  return Math.max(0, Number(process.env.AISFOR_CHAIN_MIN_VALIDATOR_STAKE || 1));
}

function areMockBlockWritesEnabled() {
  return process.env.AISFOR_CHAIN_MOCK_BLOCK_WRITES_ENABLED !== '0';
}

function mockBlockWritesLockedError() {
  const error = new Error('Legacy mock block writes are locked on the candidate network.');
  error.status = 423;
  error.code = 'MOCK_BLOCK_WRITES_LOCKED';
  return error;
}

function listBlocks() {
  return blocks;
}

function getLatestBlock() {
  return blocks[0] || null;
}

function getBlock(blockId) {
  return blocks.find((block) => (
    block.id === blockId ||
    block.blockHash === blockId ||
    String(block.height) === String(blockId)
  ));
}

function getEligibleValidators() {
  const minStake = getMinValidatorStake();
  return nodeParticipantService.listNodes().filter((node) => (
    ['active', 'warning'].includes(String(node.status || '').toLowerCase()) &&
    Number(node.lockedPoint || 0) >= minStake
  ));
}

function syncMinedTransactionRefs() {
  let changed = false;

  for (const block of blocks) {
    block.rewardIds = Array.isArray(block.rewardIds) ? block.rewardIds : [];
    block.rewardHashes = Array.isArray(block.rewardHashes) ? block.rewardHashes : [];

    for (const transactionId of block.transactionIds || []) {
      const transaction = state.transactions.find(tx => tx.id === transactionId);
      if (transaction && transaction.blockId !== block.id) {
        transaction.blockId = block.id;
        transaction.ledgerBlockHeight = block.height;
        transaction.ledgerBlockHash = block.blockHash;
        transaction.minedAt = block.createdAt;
        changed = true;
      }

      const economyEntry = (state.nodeEconomyEntries || []).find(entry => entry.transactionId === transactionId);
      if (economyEntry && economyEntry.blockId !== block.id) {
        economyEntry.blockId = block.id;
        economyEntry.blockHeight = block.height;
        economyEntry.blockHash = block.blockHash;
        economyEntry.minedAt = block.createdAt;
        changed = true;
      }
    }

    for (const rewardId of block.rewardIds || []) {
      const reward = (state.rewardEntries || []).find(entry => entry.id === rewardId);
      if (reward && reward.blockId !== block.id) {
        reward.blockId = block.id;
        reward.blockHeight = block.height;
        reward.blockHash = block.blockHash;
        reward.minedAt = block.createdAt;
        changed = true;
      }
    }
  }

  if (changed) persistentStore.writeState(state);
}

syncMinedTransactionRefs();

function getPendingProofs(limit) {
  const max = Number(limit || getAutoMineSize());
  return state.proofs
    .filter(proof => !proof.blockId)
    .slice(0, max);
}

function getMinedTransactionIds() {
  const ids = new Set();
  for (const block of blocks) {
    for (const txId of block.transactionIds || []) {
      ids.add(String(txId));
    }
  }
  return ids;
}

function getMinedRewardIds() {
  const ids = new Set();
  for (const block of blocks) {
    for (const rewardId of block.rewardIds || []) {
      ids.add(String(rewardId));
    }
  }
  return ids;
}

function rewardHash(reward = {}) {
  return reward.metadata?.idempotencyKey || reward.metadata?.externalProofHash || reward.refId || reward.id || '';
}

function getPendingRewards(limit) {
  const max = Number(limit || getAutoMineSize());
  const minedIds = getMinedRewardIds();
  return (state.rewardEntries || [])
    .filter((reward) => {
      const rewardId = String(reward.id || '');
      return rewardId && !reward.blockId && !minedIds.has(rewardId);
    })
    .slice(0, max);
}

function getPendingTransactions(limit, excludeIds = new Set()) {
  const max = Number(limit || getAutoMineSize());
  const minedIds = getMinedTransactionIds();

  return state.transactions
    .filter((transaction) => {
      const txId = String(transaction.id || '');
      return txId && !transaction.blockId && !minedIds.has(txId) && !excludeIds.has(txId);
    })
    .slice(0, max);
}

function minePendingProofs({ limit, reason = 'manual' } = {}) {
  if (!areMockBlockWritesEnabled()) {
    return {
      created: false,
      reason: 'MOCK_BLOCK_WRITES_LOCKED',
      block: null
    };
  }

  const pendingProofs = getPendingProofs(limit);
  const proofTransactionIds = new Set(pendingProofs.map(proof => String(proof.chainTransactionId || '')).filter(Boolean));
  const pendingTransactions = getPendingTransactions(limit, proofTransactionIds);
  const pendingRewards = getPendingRewards(limit);
  const transactionIds = [
    ...pendingProofs.map(proof => proof.chainTransactionId).filter(Boolean),
    ...pendingTransactions.map(transaction => transaction.id)
  ];
  const txHashes = [
    ...pendingProofs.map(proof => proof.txHash).filter(Boolean),
    ...pendingTransactions.map(transaction => transaction.txHash).filter(Boolean)
  ];

  if (!pendingProofs.length && !pendingTransactions.length && !pendingRewards.length) {
    return {
      created: false,
      reason: 'NO_PENDING_LEDGER_ITEMS',
      block: null
    };
  }

  const latestBlock = getLatestBlock();
  const block = new ChainBlock({
    height: latestBlock ? Number(latestBlock.height || 0) + 1 : 1,
    previousHash: latestBlock?.blockHash || 'GENESIS',
    proofIds: pendingProofs.map(proof => proof.id),
    transactionIds,
    rewardIds: pendingRewards.map(reward => reward.id),
    proofHashes: pendingProofs.map(proof => proof.proofHash || proof.contentHash || proof.bundleHash || proof.id),
    txHashes,
    rewardHashes: pendingRewards.map(rewardHash)
  });

  block.reason = reason;
  block.status = 'PENDING_VALIDATION';
  block.validation = {
    assignedValidatorIds: [],
    requiredVotes: 0,
    approveVotes: 0,
    rejectVotes: 0,
    finalizedAt: null
  };
  blocks.unshift(block);

  for (const proof of pendingProofs) {
    proof.blockId = block.id;
    proof.blockHeight = block.height;
    proof.blockHash = block.blockHash;
    proof.minedAt = block.createdAt;
  }

  for (const transaction of state.transactions) {
    if (!transactionIds.includes(transaction.id)) continue;
    transaction.blockId = block.id;
    transaction.ledgerBlockHeight = block.height;
    transaction.ledgerBlockHash = block.blockHash;
    transaction.minedAt = block.createdAt;
  }

  for (const entry of state.nodeEconomyEntries || []) {
    if (!transactionIds.includes(entry.transactionId)) continue;
    entry.blockId = block.id;
    entry.blockHeight = block.height;
    entry.blockHash = block.blockHash;
    entry.minedAt = block.createdAt;
  }

  for (const reward of pendingRewards) {
    reward.blockId = block.id;
    reward.blockHeight = block.height;
    reward.blockHash = block.blockHash;
    reward.minedAt = block.createdAt;
  }

  persistentStore.writeState(state);

  return {
    created: true,
    reason,
    block
  };
}

function getVotesForBlock(blockId) {
  const block = getBlock(blockId);
  if (!block) return [];
  return validatorVotes.filter((vote) => vote.blockId === block.id);
}

function assignValidators(blockId, { limit } = {}) {
  if (!areMockBlockWritesEnabled()) throw mockBlockWritesLockedError();
  const block = getBlock(blockId);
  if (!block) return null;

  const validators = getEligibleValidators().slice(0, Math.max(1, Number(limit || 3)));
  const assignedValidatorIds = validators.map(node => node.nodeId);
  const requiredVotes = Math.max(1, Math.floor(assignedValidatorIds.length / 2) + 1);

  block.validation = {
    ...(block.validation || {}),
    assignedValidatorIds,
    requiredVotes,
    approveVotes: block.validation?.approveVotes || 0,
    rejectVotes: block.validation?.rejectVotes || 0,
    assignedAt: new Date().toISOString(),
    finalizedAt: block.validation?.finalizedAt || null
  };

  if (block.status === 'SEALED_MOCK') block.status = 'PENDING_VALIDATION';
  persistentStore.writeState(state);

  return {
    block,
    validators
  };
}

async function finalizeBlockIfReady(block) {
  const validation = block.validation || {};
  const requiredVotes = Number(validation.requiredVotes || 0);
  const approveVotes = Number(validation.approveVotes || 0);
  const rejectVotes = Number(validation.rejectVotes || 0);

  if (requiredVotes <= 0 || block.status === 'FINALIZED_MOCK' || block.status === 'REJECTED_MOCK') {
    return block;
  }

  if (approveVotes >= requiredVotes) {
    const wasFinalized = block.status === 'FINALIZED_MOCK';
    block.status = 'FINALIZED_MOCK';
    block.validation.finalizedAt = new Date().toISOString();
    block.finalizedAt = block.validation.finalizedAt;

    const rewardPoint = getValidatorRewardPoint();
    if (rewardPoint > 0) {
      const approvingVotes = getVotesForBlock(block.id).filter(vote => vote.vote === 'approve');
      for (const vote of approvingVotes) {
        await nodeParticipantService.rewardNode(
          vote.validatorNodeId,
          rewardPoint,
          `validator finalized block #${block.height}`,
          {
            blockId: block.id,
            blockHeight: block.height,
            blockHash: block.blockHash,
            voteId: vote.id
          }
        );
      }
    }

    if (!wasFinalized) {
      await callbackService.createAndDeliverFinalizedCallbacks(block);
    }
  } else if (rejectVotes >= requiredVotes) {
    block.status = 'REJECTED_MOCK';
    block.validation.finalizedAt = new Date().toISOString();
    block.finalizedAt = block.validation.finalizedAt;
  }

  persistentStore.writeState(state);
  return block;
}

async function voteBlock(blockId, { validatorNodeId, vote, reason, signature, metadata } = {}) {
  if (!areMockBlockWritesEnabled()) throw mockBlockWritesLockedError();
  const block = getBlock(blockId);
  if (!block) return null;

  if (!block.validation?.assignedValidatorIds?.length) {
    assignValidators(block.id);
  }

  const assigned = new Set(block.validation?.assignedValidatorIds || []);
  if (!assigned.has(validatorNodeId)) {
    const err = new Error('VALIDATOR_NOT_ASSIGNED');
    err.status = 403;
    throw err;
  }

  const existing = validatorVotes.find(row => row.blockId === block.id && row.validatorNodeId === validatorNodeId);
  if (existing) {
    return {
      block,
      vote: existing,
      duplicate: true
    };
  }

  const voteEntry = new ValidatorVote({
    blockId: block.id,
    blockHeight: block.height,
    blockHash: block.blockHash,
    validatorNodeId,
    vote,
    reason,
    signature,
    metadata
  });

  validatorVotes.unshift(voteEntry);
  block.validation.approveVotes = getVotesForBlock(block.id).filter(row => row.vote === 'approve').length;
  block.validation.rejectVotes = getVotesForBlock(block.id).filter(row => row.vote === 'reject').length;

  await finalizeBlockIfReady(block);
  persistentStore.writeState(state);

  return {
    block,
    vote: voteEntry,
    duplicate: false
  };
}

async function mockFinalizeBlock(blockId) {
  if (!areMockBlockWritesEnabled()) throw mockBlockWritesLockedError();
  const assignment = assignValidators(blockId);
  if (!assignment) return null;

  for (const validator of assignment.validators) {
    await voteBlock(assignment.block.id, {
      validatorNodeId: validator.nodeId,
      vote: 'approve',
      reason: 'mock validator approval',
      metadata: { auto: true }
    });
  }

  return {
    block: getBlock(assignment.block.id),
    votes: getVotesForBlock(assignment.block.id)
  };
}

function autoMineIfReady() {
  const pendingCount = state.proofs.filter(proof => !proof.blockId).length + getPendingTransactions(100000).length + getPendingRewards(100000).length;
  if (pendingCount < getAutoMineSize()) {
    return {
      created: false,
      reason: 'AUTO_MINE_THRESHOLD_NOT_MET',
      pendingCount
    };
  }

  return minePendingProofs({
    limit: getAutoMineSize(),
    reason: 'auto-threshold'
  });
}

function verifyBlockChain() {
  const ordered = [...blocks].sort((a, b) => Number(a.height || 0) - Number(b.height || 0));
  const errors = [];
  let previousHash = 'GENESIS';

  for (const block of ordered) {
    if (block.previousHash !== previousHash) {
      errors.push({
        height: block.height,
        blockId: block.id,
        message: 'PREVIOUS_HASH_MISMATCH'
      });
    }

    const recalculated = new ChainBlock({
      height: block.height,
      previousHash: block.previousHash,
      proofIds: block.proofIds,
      transactionIds: block.transactionIds,
      rewardIds: block.rewardIds || [],
      proofHashes: block.proofHashes,
      txHashes: block.txHashes,
      rewardHashes: block.rewardHashes || [],
      merkleRoot: block.merkleRoot,
      createdAt: block.createdAt
    });

    if (recalculated.blockHash !== block.blockHash) {
      errors.push({
        height: block.height,
        blockId: block.id,
        message: 'BLOCK_HASH_MISMATCH'
      });
    }

    previousHash = block.blockHash;
  }

  return {
    ok: errors.length === 0,
    checked: ordered.length,
    latestBlockHash: ordered[ordered.length - 1]?.blockHash || null,
    errors
  };
}

function getBlockStats() {
  const pendingProofs = state.proofs.filter(proof => !proof.blockId);
  const pendingTransactions = getPendingTransactions(100000);
  const pendingRewards = getPendingRewards(100000);
  const latestBlock = getLatestBlock();

  return {
    totalBlocks: blocks.length,
    pendingProofs: pendingProofs.length,
    pendingTransactions: pendingTransactions.length,
    pendingRewards: pendingRewards.length,
    pendingValidationBlocks: blocks.filter(block => block.status === 'PENDING_VALIDATION').length,
    finalizedBlocks: blocks.filter(block => block.status === 'FINALIZED_MOCK').length,
    archivedLegacyBlocks: blocks.filter(block => block.status === 'ARCHIVED_LEGACY_MOCK').length,
    quarantinedLegacyBlocks: blocks.filter(block => block.status === 'QUARANTINED_LEGACY_MOCK').length,
    eligibleValidators: getEligibleValidators().length,
    latestHeight: latestBlock?.height || 0,
    latestBlockHash: latestBlock?.blockHash || null,
    autoMineSize: getAutoMineSize()
  };
}

module.exports = {
  listBlocks,
  getBlock,
  getLatestBlock,
  getPendingProofs,
  getPendingTransactions,
  getPendingRewards,
  minePendingProofs,
  autoMineIfReady,
  areMockBlockWritesEnabled,
  getEligibleValidators,
  getVotesForBlock,
  assignValidators,
  voteBlock,
  mockFinalizeBlock,
  verifyBlockChain,
  getBlockStats
};
