import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {altair, Slot} from "@lodestar/types";
import {BeaconConfig} from "@lodestar/config";
import {LightClientSnapshotFast, LightClientStoreFast} from "../../../src/types.js";
import {assertValidLightClientUpdate} from "../../../src/validation.js";
import {deserializeSyncCommittee, isEmptyHeader, sumBits} from "../../../src/utils/utils.js";
import {computeSyncPeriodAtSlot} from "../../../src/utils/clock.js";

//
// A lightclient has two types of syncing:
// - Committee sync: Retrieve all SyncCommittee pubkeys for a given period.
//   To do that safely the lightclient has to retrieve the syncCommittee of each period
//   between its latest trusted and up to the target. The lightclient doesn't care about the slot of
//   the updates as long as there's at least one per period.
//
//   GET /eth/v1/lightclient/best-update/:periods
//
//   The definition of "best-update" is vague so nodes have the freedom to optimize what to store.
//   A possible strategy is to persist the latest LightclientUpdate that has the most bits within a period.
//
//   How to start from genesis or a known state root? The lightclient will request the node a proof of
//   the NextSyncCommittee at the trusted state root and then sync from computePeriodAtSlot(trustedSlot)
//
// - Head sync: To query state beyond the latest finalized checkpoint, and for a known SyncCommittee that period N
//   the lightclient can request headers signed by a known SyncCommittee.
//
//   GET /eth/v1/lightclient/latest-header/:period
//   {
//     header: phase0.BeaconBlockHeader;
//     syncCommitteeBits: BitVector;
//     syncCommitteeSignature: primitive.BLSSignature;
//     forkVersion: primitive.Version;
//   }
//
//   Nodes should keep the latest header at a specific period, or even just the latest period and 404 the rest.
//   All past data can be then retrieved with proofs, there no point in serving old updates.

// Sync process for lightclient:
// 1. Start from a trusted state root. If the lightclient has an existing store jump to 3
// 2. Request a proof for currentSyncCommittee and nextSyncCommittee at the trusted state root
// 3. Compute the list of sequential periods between the trusted state root and the clock = periods.
//    GET /eth/v1/lightclient/best-update/{periods}
// 4. Verify and apply each update sequentially.
// 5. At the end of each clock period jump to 3 and repeat

/**
 * A light client maintains its state in a store object of type LightClientStore and receives update objects of type LightClientUpdate.
 * Every update triggers process_light_client_update(store, update, current_slot) where current_slot is the current slot based on some local clock.
 * Spec v1.0.1
 */
export function processLightClientUpdate(
  config: BeaconConfig,
  store: LightClientStoreFast,
  update: altair.LightClientUpdate,
  currentSlot: Slot
): void {
  // TODO - TEMP
  const syncCommittee = store.snapshot.nextSyncCommittee;
  assertValidLightClientUpdate(config, syncCommittee, update);

  const syncPeriod = computeSyncPeriodAtSlot(update.attestedHeader.beacon.slot);
  const prevBestUpdate = store.bestUpdates.get(syncPeriod);
  if (!prevBestUpdate || isBetterUpdate(prevBestUpdate, update)) {
    store.bestUpdates.set(syncPeriod, update);
  }

  const updateTimeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD;

  // Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof.
  // Note that (2) means that the current light client design needs finality.
  // It may be changed to re-organizable light client design. See the on-going issue https://github.com/ethereum/consensus-specs/issues/2315.
  if (
    sumBits(update.syncAggregate.syncCommitteeBits) * 3 >= update.syncAggregate.syncCommitteeBits.bitLen * 2 &&
    !isEmptyHeader(update.finalizedHeader.beacon)
  ) {
    applyLightClientUpdate(store.snapshot, update);
    store.bestUpdates.delete(syncPeriod);
  }

  // Forced best update when the update timeout has elapsed
  else if (currentSlot > store.snapshot.header.beacon.slot + updateTimeout) {
    const prevSyncPeriod = computeSyncPeriodAtSlot(store.snapshot.header.beacon.slot);
    const bestUpdate = store.bestUpdates.get(prevSyncPeriod);
    if (bestUpdate) {
      applyLightClientUpdate(store.snapshot, bestUpdate);
      store.bestUpdates.delete(prevSyncPeriod);
    }
  }
}

/**
 * Spec v1.0.1
 */
export function applyLightClientUpdate(snapshot: LightClientSnapshotFast, update: altair.LightClientUpdate): void {
  const snapshotPeriod = computeSyncPeriodAtSlot(snapshot.header.beacon.slot);
  const updatePeriod = computeSyncPeriodAtSlot(update.attestedHeader.beacon.slot);
  if (updatePeriod < snapshotPeriod) {
    throw Error("Cannot rollback sync period");
  }
  if (updatePeriod === snapshotPeriod + 1) {
    snapshot.currentSyncCommittee = snapshot.nextSyncCommittee;
    snapshot.nextSyncCommittee = deserializeSyncCommittee(update.nextSyncCommittee);
  }
  snapshot.header = update.attestedHeader;
}

/**
 * Returns the update with more bits. On ties, newUpdate is the better
 *
 * Spec v1.0.1
 * ```python
 * max(store.valid_updates, key=lambda update: sum(update.sync_committee_bits)))
 * ```
 */
export function isBetterUpdate(prevUpdate: altair.LightClientUpdate, newUpdate: altair.LightClientUpdate): boolean {
  return sumBits(newUpdate.syncAggregate.syncCommitteeBits) >= sumBits(prevUpdate.syncAggregate.syncCommitteeBits);
}
