import {BitArray, deserializeUint8ArrayBitListFromBytes} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {
  BYTES_PER_FIELD_ELEMENT,
  FIELD_ELEMENTS_PER_BLOB,
  ForkName,
  ForkPostDeneb,
  ForkSeq,
  MAX_COMMITTEES_PER_SLOT,
  isForkPostElectra,
  isForkPostGloas,
} from "@lodestar/params";
import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types";

export type BlockRootHex = RootHex;
// pre-electra, AttestationData is used to cache attestations
export type AttDataBase64 = string;
// electra, CommitteeBits
export type CommitteeBitsBase64 = string;
/** `attestation.data.index` from gossip-serialized attestations / aggregates */
export type AttDataIndex = number;

// pre-electra
// class Attestation(Container):
//   aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] - offset 4
//   data: AttestationData - target data - 128
//   signature: BLSSignature - 96

// electra
// class Attestation(Container):
//   aggregation_bits: BitList[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] - offset 4
//   data: AttestationData - target data - 128
//   signature: BLSSignature - 96
//   committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT]
// electra
// class SingleAttestation(Container):
//   committeeIndex: CommitteeIndex - data 8
//   attesterIndex: ValidatorIndex - data 8
//   data: AttestationData - data 128
//   signature: BLSSignature - data 96
//
// for all forks
// class AttestationData(Container): 128 bytes fixed size
//   slot: Slot                - data 8
//   index: CommitteeIndex     - data 8
//   beacon_block_root: Root   - data 32
//   source: Checkpoint        - data 40
//   target: Checkpoint        - data 40

const VARIABLE_FIELD_OFFSET = 4;
const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8;
export const ROOT_SIZE = 32;
const SLOT_SIZE = 8;
const COMMITTEE_INDEX_SIZE = 8;
const ATTESTATION_DATA_SIZE = 128;
// MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte
const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1);
const SIGNATURE_SIZE = 96;
const SINGLE_ATTESTATION_ATTDATA_OFFSET = 8 + 8;
const SINGLE_ATTESTATION_SLOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET;
const SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET = 0;
const SINGLE_ATTESTATION_DATA_INDEX_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8;
const SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET = 8;
const SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8 + 8;
const SINGLE_ATTESTATION_SIGNATURE_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE;
const SINGLE_ATTESTATION_SIZE = SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE;

// shared Buffers to convert bytes to hex/base64
const blockRootBuf = Buffer.alloc(ROOT_SIZE);
const attDataBuf = Buffer.alloc(ATTESTATION_DATA_SIZE);
const committeeBitsDataBuf = Buffer.alloc(COMMITTEE_BITS_SIZE);

/**
 * Extract slot from attestation serialized bytes.
 * Return null if data is not long enough to extract slot.
 */
export function getSlotFromAttestationSerialized(data: Uint8Array): Slot | null {
  if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, VARIABLE_FIELD_OFFSET);
}

/**
 * Extract block root from attestation serialized bytes.
 * Return null if data is not long enough to extract block root.
 */
export function getBlockRootFromAttestationSerialized(data: Uint8Array): BlockRootHex | null {
  if (data.length < ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) {
    return null;
  }

  blockRootBuf.set(
    data.subarray(ATTESTATION_BEACON_BLOCK_ROOT_OFFSET, ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE)
  );
  return "0x" + blockRootBuf.toString("hex");
}

/**
 * Extract attestation data base64 from all forks' attestation serialized bytes.
 * Return null if data is not long enough to extract attestation data.
 */
export function getAttDataFromAttestationSerialized(data: Uint8Array): AttDataBase64 | null {
  if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) {
    return null;
  }

  // base64 is a bit efficient than hex
  attDataBuf.set(data.subarray(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE));
  return attDataBuf.toString("base64");
}

/**
 * Extract AttDataBase64 from `beacon_attestation` gossip message serialized bytes.
 * This is used for GossipQueue.
 */
export function getBeaconAttestationGossipIndex(fork: ForkName, data: Uint8Array): AttDataBase64 | null {
  return ForkSeq[fork] >= ForkSeq.electra
    ? getAttDataFromSingleAttestationSerialized(data)
    : getAttDataFromAttestationSerialized(data);
}

/**
 * Extract slot from `beacon_attestation` gossip message serialized bytes.
 */
export function getSlotFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): Slot | null {
  return ForkSeq[fork] >= ForkSeq.electra
    ? getSlotFromSingleAttestationSerialized(data)
    : getSlotFromAttestationSerialized(data);
}

/**
 * Extract block root from `beacon_attestation` gossip message serialized bytes.
 */
export function getBlockRootFromBeaconAttestationSerialized(fork: ForkName, data: Uint8Array): BlockRootHex | null {
  return ForkSeq[fork] >= ForkSeq.electra
    ? getBlockRootFromSingleAttestationSerialized(data)
    : getBlockRootFromAttestationSerialized(data);
}

/**
 * Extract aggregation bits from attestation serialized bytes.
 * Return null if data is not long enough to extract aggregation bits.
 * Pre-electra attestation only
 */
export function getAggregationBitsFromAttestationSerialized(data: Uint8Array): BitArray | null {
  const aggregationBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE;

  if (data.length < aggregationBitsStartIndex) {
    return null;
  }

  const {uint8Array, bitLen} = deserializeUint8ArrayBitListFromBytes(data, aggregationBitsStartIndex, data.length);
  return new BitArray(uint8Array, bitLen);
}

/**
 * Extract signature from attestation serialized bytes.
 * Return null if data is not long enough to extract signature.
 */
export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSignature | null {
  const signatureStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE;

  if (data.length < signatureStartIndex + SIGNATURE_SIZE) {
    return null;
  }

  return data.subarray(signatureStartIndex, signatureStartIndex + SIGNATURE_SIZE);
}

/**
 * Extract slot from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract slot.
 */
export function getSlotFromSingleAttestationSerialized(data: Uint8Array): Slot | null {
  if (data.length !== SINGLE_ATTESTATION_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, SINGLE_ATTESTATION_SLOT_OFFSET);
}

/**
 * Extract committee index from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract the committee index.
 */
export function getCommitteeIndexFromSingleAttestationSerialized(
  fork: ForkName,
  data: Uint8Array
): CommitteeIndex | null {
  if (isForkPostElectra(fork)) {
    if (data.length !== SINGLE_ATTESTATION_SIZE) {
      return null;
    }

    return getIndexFromOffset(data, SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET);
  }

  if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE + COMMITTEE_INDEX_SIZE) {
    return null;
  }

  return getIndexFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE);
}

/**
 * Extract data index from SingleAttestation serialized bytes.
 * Post-gloas, `data.index` field is repurposed:
 *   - 0 - payload was not available (or attestation is same-slot, where availability is not yet known)
 *   - 1 - payload was available
 * Return null if data is not long enough to extract the index.
 */
export function getDataIndexFromSingleAttestationSerialized(fork: ForkName, data: Uint8Array): AttDataIndex | null {
  if (isForkPostElectra(fork)) {
    if (data.length !== SINGLE_ATTESTATION_SIZE) {
      return null;
    }

    return getIndexFromOffset(data, SINGLE_ATTESTATION_DATA_INDEX_OFFSET);
  }

  if (data.length < VARIABLE_FIELD_OFFSET + SLOT_SIZE + COMMITTEE_INDEX_SIZE) {
    return null;
  }

  return getIndexFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE);
}

/**
 * Extract attester index from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract index.
 */
export function getAttesterIndexFromSingleAttestationSerialized(data: Uint8Array): ValidatorIndex | null {
  if (data.length !== SINGLE_ATTESTATION_SIZE) {
    return null;
  }

  return getIndexFromOffset(data, SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET);
}

/**
 * Extract block root from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract block root.
 */
export function getBlockRootFromSingleAttestationSerialized(data: Uint8Array): BlockRootHex | null {
  if (data.length !== SINGLE_ATTESTATION_SIZE) {
    return null;
  }

  blockRootBuf.set(
    data.subarray(SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET, SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE)
  );
  return `0x${blockRootBuf.toString("hex")}`;
}

/**
 * Extract attestation data base64 from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract attestation data.
 */
export function getAttDataFromSingleAttestationSerialized(data: Uint8Array): AttDataBase64 | null {
  if (data.length !== SINGLE_ATTESTATION_SIZE) {
    return null;
  }

  // base64 is a bit efficient than hex
  attDataBuf.set(
    data.subarray(SINGLE_ATTESTATION_ATTDATA_OFFSET, SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE)
  );
  return attDataBuf.toString("base64");
}

/**
 * Extract signature from SingleAttestation serialized bytes.
 * Return null if data is not long enough to extract signature.
 */
export function getSignatureFromSingleAttestationSerialized(data: Uint8Array): BLSSignature | null {
  if (data.length !== SINGLE_ATTESTATION_SIZE) {
    return null;
  }

  return data.subarray(SINGLE_ATTESTATION_SIGNATURE_OFFSET, SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE);
}

//
// class SignedAggregateAndProof(Container):
//    message: AggregateAndProof - offset 4
//    signature: BLSSignature    - data 96

// class AggregateAndProof(Container)
//    aggregatorIndex: ValidatorIndex - data 8
//    aggregate: Attestation          - offset 4
//    selectionProof: BLSSignature    - data 96

const AGGREGATE_AND_PROOF_OFFSET = 4 + 96;
const AGGREGATE_OFFSET = AGGREGATE_AND_PROOF_OFFSET + 8 + 4 + 96;
const SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET = AGGREGATE_OFFSET + VARIABLE_FIELD_OFFSET;
const SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE;
const SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + 8 + 8;

/**
 * Extract slot from signed aggregate and proof serialized bytes
 * Return null if data is not long enough to extract slot
 * This works for both phase + electra
 */
export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array): Slot | null {
  if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET);
}

/**
 * Extract block root from signed aggregate and proof serialized bytes
 * Return null if data is not long enough to extract block root
 * This works for both phase + electra
 */
export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Array): BlockRootHex | null {
  if (data.length < SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE) {
    return null;
  }

  blockRootBuf.set(
    data.subarray(
      SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET,
      SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE
    )
  );
  return "0x" + blockRootBuf.toString("hex");
}

/**
 * Extract data index from signed aggregate and proof serialized bytes.
 * Return null if data is not long enough to extract the index.
 * This works for both phase0 + electra (index is in attestation data at the same offset).
 */
export function getDataIndexFromSignedAggregateAndProofSerialized(data: Uint8Array): AttDataIndex | null {
  if (data.length < SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET + COMMITTEE_INDEX_SIZE) {
    return null;
  }

  return getIndexFromOffset(data, SIGNED_AGGREGATE_AND_PROOF_ATTESTATION_DATA_INDEX_OFFSET);
}

/**
 * Extract AttestationData base64 from SignedAggregateAndProof for electra
 * Return null if data is not long enough
 */
export function getAttDataFromSignedAggregateAndProofElectra(data: Uint8Array): AttDataBase64 | null {
  const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET;
  const endIndex = startIndex + ATTESTATION_DATA_SIZE;

  if (data.length < endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE) {
    return null;
  }
  attDataBuf.set(data.subarray(startIndex, endIndex));
  return attDataBuf.toString("base64");
}

/**
 * Extract CommitteeBits base64 from SignedAggregateAndProof for electra
 * Return null if data is not long enough
 */
export function getCommitteeBitsFromSignedAggregateAndProofElectra(data: Uint8Array): CommitteeBitsBase64 | null {
  const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE;
  const endIndex = startIndex + COMMITTEE_BITS_SIZE;

  if (data.length < endIndex) {
    return null;
  }

  committeeBitsDataBuf.set(data.subarray(startIndex, endIndex));
  return committeeBitsDataBuf.toString("base64");
}

/**
 * Extract attestation data base64 from signed aggregate and proof serialized bytes.
 * Return null if data is not long enough to extract attestation data.
 */
export function getAttDataFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null {
  if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) {
    return null;
  }

  // base64 is a bit efficient than hex
  attDataBuf.set(
    data.subarray(
      SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET,
      SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE
    )
  );
  return attDataBuf.toString("base64");
}

/**
 * 4 + 96 = 100
 * ```
 * class SignedBeaconBlock(Container):
 *   message: BeaconBlock [offset - 4 bytes]
 *   signature: BLSSignature [fixed - 96 bytes]
 *
 * class BeaconBlock(Container):
 *   slot: Slot [fixed - 8 bytes]
 *   proposer_index: ValidatorIndex
 *   parent_root: Root
 *   state_root: Root
 *   body: BeaconBlockBody
 * ```
 */
const SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE;
// proposer_index is ValidatorIndex = uint64 = 8 bytes
const PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + SLOT_SIZE + 8;

export function getSlotFromSignedBeaconBlockSerialized(data: Uint8Array): Slot | null {
  if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK);
}

export function getParentRootFromSignedBeaconBlockSerialized(data: Uint8Array): RootHex | null {
  if (data.length < PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + ROOT_SIZE) {
    return null;
  }
  blockRootBuf.set(
    data.subarray(
      PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK,
      PARENT_ROOT_BYTES_POSITION_IN_SIGNED_BEACON_BLOCK + ROOT_SIZE
    )
  );
  return `0x${blockRootBuf.toString("hex")}`;
}

/**
 * Extract parentBlockHash from a GLOAS SignedBeaconBlock by navigating the SSZ offset pointer
 * to the embedded SignedExecutionPayloadBid.
 *
 * Layout (bytes from start of SignedBeaconBlock):
 *   [0..4)    message offset
 *   [4..100)  signature (96 B)
 *   [100..184) BeaconBlock fixed section: slot(8)+proposer_index(8)+parent_root(32)+state_root(32)+body_offset(4)
 *   [184..)   BeaconBlockBody
 *
 * BeaconBlockBody (GLOAS) fixed section before signedExecutionPayloadBid offset pointer:
 *   randaoReveal(96) + eth1Data(72) + graffiti(32)
 *   + proposerSlashings(4) + attesterSlashings(4) + attestations(4) + deposits(4) + voluntaryExits(4)
 *   + syncAggregate(160) + blsToExecutionChanges(4) = 384 bytes
 *
 * The 4-byte pointer at byte 568 (= 184+384) gives the offset of SignedExecutionPayloadBid
 * within BeaconBlockBody. parentBlockHash is at that bid's byte 100 (after offset+sig).
 */
// BeaconBlock body starts after: msg_offset(4) + sig(96) + slot(8) + proposer_index(8) + parent_root(32) + state_root(32) + body_offset_ptr(4)
const GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK =
  VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + SLOT_SIZE + 8 + ROOT_SIZE + ROOT_SIZE + VARIABLE_FIELD_OFFSET; // = 184
const GLOAS_SIGNED_BID_OFFSET_POINTER_IN_BODY = 96 + 72 + 32 + 4 + 4 + 4 + 4 + 4 + 160 + 4; // = 384
const GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK =
  GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK + GLOAS_SIGNED_BID_OFFSET_POINTER_IN_BODY; // = 568
// Within SignedExecutionPayloadBid, parentBlockHash is at byte 100 (msg_offset:4 + sig:96)
const PARENT_BLOCK_HASH_OFFSET_IN_SIGNED_BID = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE; // = 100

// CAUTION: update offsets if BeaconBlockBody fixed fields change after Gloas
export function getParentBlockHashFromGloasSignedBeaconBlockSerialized(data: Uint8Array): RootHex | null {
  if (data.length < GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + VARIABLE_FIELD_OFFSET) {
    return null;
  }
  const bidOffset =
    data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK] |
    (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 1] << 8) |
    (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 2] << 16) |
    (data[GLOAS_SIGNED_BID_OFFSET_POINTER_IN_SIGNED_BEACON_BLOCK + 3] << 24);

  const parentBlockHashStart =
    GLOAS_BODY_START_IN_SIGNED_BEACON_BLOCK + bidOffset + PARENT_BLOCK_HASH_OFFSET_IN_SIGNED_BID;

  if (data.length < parentBlockHashStart + ROOT_SIZE) {
    return null;
  }

  blockRootBuf.set(data.subarray(parentBlockHashStart, parentBlockHashStart + ROOT_SIZE));
  return `0x${blockRootBuf.toString("hex")}`;
}

/**
 * class BlobSidecar(Container):
 *  index: BlobIndex [fixed - 8 bytes ],
 *  blob: Blob, BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB
 *  kzgCommitment: Bytes48,
 *  kzgProof: Bytes48,
 *  signedBlockHeader:
 *    slot: 8 bytes
 */

const SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR = 8 + BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB + 48 + 48;

export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null {
  if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_BLOB_SIDECAR);
}

/**
 * Pre-Gloas DataColumnSidecar:
 * {
 *   index: ColumnIndex [fixed - 8 bytes],
 *   column: DataColumn (offset - 4 bytes),
 *   kzgCommitments: (offset - 4 bytes),
 *   kzgProofs: (offset - 4 bytes),
 *   signedBlockHeader: (offset - 4 bytes) -> slot at variable offset after fixed header
 *   kzgCommitmentsInclusionProof: (offset - 4 bytes),
 * }
 * Post-Gloas DataColumnSidecar:
 * {
 *   index: ColumnIndex [8 bytes],
 *   column: DataColumn (offset - 4 bytes),
 *   kzgProofs: (offset - 4 bytes),
 *   slot: Slot [8 bytes] - at offset 16,
 *   beaconBlockRoot: Root [32 bytes] - at offset 24,
 * }
 */
const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS = 20;
const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS = 16;
const BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 24;

export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array, fork: ForkName): Slot | null {
  const offset = isForkPostGloas(fork)
    ? SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS
    : SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS;

  if (data.length < offset + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, offset);
}

export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Array): RootHex | null {
  if (data.length < BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE) {
    return null;
  }

  blockRootBuf.set(
    data.subarray(
      BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR,
      BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE
    )
  );
  return "0x" + blockRootBuf.toString("hex");
}

/**
 * SignedExecutionPayloadEnvelope SSZ Layout:
 * ├─ 4 bytes: message offset (points to byte 100)
 * ├─ 96 bytes: signature
 * └─ ExecutionPayloadEnvelope (starts at byte 100):
 *    ├─ 4 bytes: payload offset
 *    ├─ 4 bytes: executionRequests offset
 *    ├─ 8 bytes: builderIndex          (offset 108-115)
 *    ├─ 32 bytes: beaconBlockRoot      (offset 116-147)
 *    ├─ 32 bytes: parentBeaconBlockRoot (offset 148-179) — new in Gloas alpha.6 (consensus-specs#5152)
 *    └─ variable: payload data (starts at envelope + 80)
 *       └─ ExecutionPayload fixed portion includes slotNumber at offset 532
 */
const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4;
const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE = 96;
const EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET = 4;
const EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET = 4;
const EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE = 8;

const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
  SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET +
  SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE +
  EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET +
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116

// Envelope fixed portion: payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) + parentBeaconBlockRoot(32) = 80
const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE =
  EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET +
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE +
  ROOT_SIZE +
  ROOT_SIZE; // 80

// slotNumber offset within ExecutionPayload fixed portion:
// parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) +
// prevRandao(32) + blockNumber(8) + gasLimit(8) + gasUsed(8) + timestamp(8) +
// extraData_offset(4) + baseFeePerGas(32) + blockHash(32) + transactions_offset(4) +
// withdrawals_offset(4) + blobGasUsed(8) + excessBlobGas(8) + blockAccessList_offset(4) = 532
const SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD = 532;

// Payload data starts right after the envelope's fixed portion
const ENVELOPE_START_IN_SIGNED =
  SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100

const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
  ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 80 + 532 = 712

export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null {
  if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE);
}

export function getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): RootHex | null {
  if (data.length < BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE) {
    return null;
  }

  blockRootBuf.set(
    data.subarray(
      BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE,
      BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE
    )
  );
  return "0x" + blockRootBuf.toString("hex");
}

/**
 * BeaconState of all forks (up until Electra, check with new forks)
 * class BeaconState(Container):
 *   genesis_time: uint64                    - 8 bytes
 *   genesis_validators_root: Root           - 32 bytes
 *   slot: Slot                              - 8 bytes
 *   fork: Fork                              - 16 bytes
 *   latest_block_header: BeaconBlockHeader  - fixed size
 *     slot: Slot                            - 8 bytes
 *
 */

const BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE = 8 + 32 + 8 + 16;
export function getLastProcessedSlotFromBeaconStateSerialized(data: Uint8Array): Slot | null {
  if (data.length < BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE + SLOT_SIZE) {
    return null;
  }

  return getSlotFromOffset(data, BLOCK_HEADER_SLOT_BYTES_POSITION_IN_BEACON_STATE);
}

const SLOT_BYTES_POSITION_IN_BEACON_STATE = 8 + 32;
export function getSlotFromBeaconStateSerialized(data: Uint8Array): Slot | null {
  if (data.length < SLOT_BYTES_POSITION_IN_BEACON_STATE) {
    return null;
  }

  return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_BEACON_STATE);
}

/**
 * PayloadAttestationMessage: {
 *   validatorIndex: ValidatorIndex (8 bytes)
 *   data: PayloadAttestationData {
 *     beaconBlockRoot: Root (32 bytes)  ← offset 8
 *     slot: Slot (8 bytes)              ← offset 40
 *     payloadPresent: Boolean (1 byte)
 *     blobDataAvailable: Boolean (1 byte)
 *   }
 *   signature: BLSSignature (96 bytes)
 * }
 * Fully fixed-size container, no offset table.
 */
const PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET = 8;
const PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET = 8 + ROOT_SIZE; // 40
const PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET = PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET + SLOT_SIZE; // 48

export function getSlotFromPayloadAttestationMessageSerialized(data: Uint8Array): Slot | null {
  if (data.length < PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET + SLOT_SIZE) {
    return null;
  }
  return getSlotFromOffset(data, PAYLOAD_ATTESTATION_MESSAGE_SLOT_OFFSET);
}

export function getPayloadPresentFromPayloadAttestationMessageSerialized(data: Uint8Array): boolean | null {
  if (data.length < PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET + 1) {
    return null;
  }
  return data[PAYLOAD_ATTESTATION_MESSAGE_PAYLOAD_PRESENT_OFFSET] !== 0;
}

export function getBlockRootFromPayloadAttestationMessageSerialized(data: Uint8Array): RootHex | null {
  if (data.length < PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE) {
    return null;
  }
  blockRootBuf.set(
    data.subarray(
      PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET,
      PAYLOAD_ATTESTATION_MESSAGE_BEACON_BLOCK_ROOT_OFFSET + ROOT_SIZE
    )
  );
  return `0x${blockRootBuf.toString("hex")}`;
}

/**
 * SignedExecutionPayloadBid: {message: ExecutionPayloadBid (variable), signature: BLSSignature (96 bytes)}
 *   Fixed part: 4-byte offset + 96-byte signature = 100 bytes
 *   message data starts at byte 100
 *
 * ExecutionPayloadBid fixed fields (in order):
 *   parentBlockHash: Bytes32      (32 bytes)
 *   parentBlockRoot: Root         (32 bytes)
 *   blockHash: Bytes32            (32 bytes)
 *   prevRandao: Bytes32           (32 bytes)
 *   feeRecipient: ExecutionAddress(20 bytes)
 *   gasLimit: UintBn64            (8 bytes)
 *   builderIndex: BuilderIndex    (8 bytes)
 *   slot: Slot                    (8 bytes)  ← absolute offset 264
 */
const SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET = VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE; // 100
const SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET =
  SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE; // 132
const SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET =
  VARIABLE_FIELD_OFFSET + SIGNATURE_SIZE + 32 + 32 + 32 + 32 + 20 + 8 + 8; // 264

export function getSlotFromSignedExecutionPayloadBidSerialized(data: Uint8Array): Slot | null {
  if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET + SLOT_SIZE) {
    return null;
  }
  return getSlotFromOffset(data, SIGNED_EXECUTION_PAYLOAD_BID_SLOT_OFFSET);
}

export function getParentBlockHashFromSignedExecutionPayloadBidSerialized(data: Uint8Array): RootHex | null {
  if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE) {
    return null;
  }
  blockRootBuf.set(
    data.subarray(
      SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET,
      SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_HASH_OFFSET + ROOT_SIZE
    )
  );
  return `0x${blockRootBuf.toString("hex")}`;
}

export function getParentBlockRootFromSignedExecutionPayloadBidSerialized(data: Uint8Array): RootHex | null {
  if (data.length < SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET + ROOT_SIZE) {
    return null;
  }
  blockRootBuf.set(
    data.subarray(
      SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET,
      SIGNED_EXECUTION_PAYLOAD_BID_PARENT_BLOCK_ROOT_OFFSET + ROOT_SIZE
    )
  );
  return `0x${blockRootBuf.toString("hex")}`;
}

/**
 * Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis
 *
 * If the high bytes are not zero, return null
 */
function getSlotFromOffset(data: Uint8Array, offset: number): Slot | null {
  return checkSlotHighBytes(data, offset) ? getSlotFromOffsetTrusted(data, offset) : null;
}

/**
 * Alias of `getSlotFromOffset` for readability
 */
function getIndexFromOffset(data: Uint8Array, offset: number): (ValidatorIndex | CommitteeIndex) | null {
  return getSlotFromOffset(data, offset);
}

/**
 * Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis
 */
function getSlotFromOffsetTrusted(data: Uint8Array, offset: number): Slot {
  return (data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)) >>> 0;
}

function checkSlotHighBytes(data: Uint8Array, offset: number): boolean {
  return (data[offset + 4] | data[offset + 5] | data[offset + 6] | data[offset + 7]) === 0;
}

export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized(
  config: ChainForkConfig,
  blockBytes: Uint8Array
): number {
  const slot = getSlotFromSignedBeaconBlockSerialized(blockBytes);
  if (slot === null) throw new Error("Can not parse the slot from block bytes");

  if (config.getForkSeq(slot) < ForkSeq.deneb) return 0;
  const forkName = config.getForkName(slot);

  if (isForkPostGloas(forkName)) {
    // Gloas stores commitments under signedExecutionPayloadBid.message.blobKzgCommitments.
    // Navigate the offset chain: SignedBeaconBlock → message → body → signedExecutionPayloadBid → message → blobKzgCommitments
    const {SignedBeaconBlock: GloasSignedBlock, BeaconBlock: GloasBlock, BeaconBlockBody: GloasBody} = ssz[forkName];
    const {SignedExecutionPayloadBid, ExecutionPayloadBid} = ssz[forkName];
    const commitmentSize = ssz.deneb.KZGCommitment.fixedSize;

    const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength);

    const signedBlockRanges = GloasSignedBlock.getFieldRanges(view, 0, blockBytes.length);
    const messageIdx = Object.keys(GloasSignedBlock.fields).indexOf("message");
    const messageRange = signedBlockRanges[messageIdx];

    const blockRanges = GloasBlock.getFieldRanges(view, messageRange.start, messageRange.end);
    const bodyIdx = Object.keys(GloasBlock.fields).indexOf("body");
    const bodyRange = blockRanges[bodyIdx];
    const bodyStart = messageRange.start + bodyRange.start;
    const bodyEnd = messageRange.start + bodyRange.end;

    const bodyRanges = GloasBody.getFieldRanges(view, bodyStart, bodyEnd);
    const bidIdx = Object.keys(GloasBody.fields).indexOf("signedExecutionPayloadBid");
    const bidRange = bodyRanges[bidIdx];
    const bidStart = bodyStart + bidRange.start;
    const bidEnd = bodyStart + bidRange.end;

    const bidRanges = SignedExecutionPayloadBid.getFieldRanges(view, bidStart, bidEnd);
    const bidMsgIdx = Object.keys(SignedExecutionPayloadBid.fields).indexOf("message");
    const bidMsgRange = bidRanges[bidMsgIdx];
    const bidMsgStart = bidStart + bidMsgRange.start;
    const bidMsgEnd = bidStart + bidMsgRange.end;

    const execBidRanges = ExecutionPayloadBid.getFieldRanges(view, bidMsgStart, bidMsgEnd);
    const commitmentsIdx = Object.keys(ExecutionPayloadBid.fields).indexOf("blobKzgCommitments");
    const commitmentsRange = execBidRanges[commitmentsIdx];

    const start = bidMsgStart + commitmentsRange.start;
    const end = bidMsgStart + commitmentsRange.end;
    return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize);
  }

  const {SignedBeaconBlock, BeaconBlock, BeaconBlockBody, KZGCommitment} = ssz[forkName as ForkPostDeneb];

  const view = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength);
  const singedBlockFieldRanges = SignedBeaconBlock.getFieldRanges(view, 0, blockBytes.length);
  const messageIndex = Object.keys(SignedBeaconBlock.fields).indexOf("message");
  const messageRange = singedBlockFieldRanges[messageIndex];

  const blockFieldRanges = BeaconBlock.getFieldRanges(view, messageRange.start, messageRange.end);
  const bodyIndex = Object.keys(BeaconBlock.fields).indexOf("body");
  const bodyRange = blockFieldRanges[bodyIndex];

  const bodyFieldRanges = BeaconBlockBody.getFieldRanges(
    view,
    messageRange.start + bodyRange.start,
    messageRange.end + bodyRange.end
  );
  const kzgCommitmentsIndex = Object.keys(BeaconBlockBody.fields).indexOf("blobKzgCommitments");
  const kzgCommitmentsRange = bodyFieldRanges[kzgCommitmentsIndex];
  const commitmentSize = KZGCommitment.fixedSize;

  const end = messageRange.end + bodyRange.end + kzgCommitmentsRange.end;
  const start = messageRange.start + bodyRange.start + kzgCommitmentsRange.start;

  return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize);
}
