# Nimbus - Portal Network
# Copyright (c) 2022-2024 Status Research & Development GmbH
# Licensed and distributed under either of
#   * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
#   * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

{.push raises: [].}

import
  chronicles,
  eth/p2p/discoveryv5/random2,
  beacon_chain/gossip_processing/light_client_processor,
  beacon_chain/spec/datatypes/altair,
  beacon_chain/beacon_clock,
  "."/[beacon_init_loader, beacon_network, beacon_light_client_manager]

export LightClientFinalizationMode, beacon_network, beacon_light_client_manager

logScope:
  topics = "beacon_lc"

type
  LightClientHeaderCallback* = proc(
    lightClient: LightClient, header: ForkedLightClientHeader
  ) {.gcsafe, raises: [].}

  LightClient* = ref object
    network*: BeaconNetwork
    cfg: RuntimeConfig
    forkDigests: ref ForkDigests
    getBeaconTime*: GetBeaconTimeFn
    store*: ref ForkedLightClientStore
    processor*: ref LightClientProcessor
    manager: LightClientManager
    onFinalizedHeader*, onOptimisticHeader*: LightClientHeaderCallback
    trustedBlockRoot*: Opt[Eth2Digest]

func getFinalizedHeader*(lightClient: LightClient): ForkedLightClientHeader =
  withForkyStore(lightClient.store[]):
    when lcDataFork > LightClientDataFork.None:
      var header = ForkedLightClientHeader(kind: lcDataFork)
      header.forky(lcDataFork) = forkyStore.finalized_header
      header
    else:
      default(ForkedLightClientHeader)

func getOptimisticHeader*(lightClient: LightClient): ForkedLightClientHeader =
  withForkyStore(lightClient.store[]):
    when lcDataFork > LightClientDataFork.None:
      var header = ForkedLightClientHeader(kind: lcDataFork)
      header.forky(lcDataFork) = forkyStore.optimistic_header
      header
    else:
      default(ForkedLightClientHeader)

proc new*(
    T: type LightClient,
    network: BeaconNetwork,
    rng: ref HmacDrbgContext,
    dumpEnabled: bool,
    dumpDirInvalid, dumpDirIncoming: string,
    cfg: RuntimeConfig,
    forkDigests: ref ForkDigests,
    getBeaconTime: GetBeaconTimeFn,
    genesis_validators_root: Eth2Digest,
    finalizationMode: LightClientFinalizationMode,
): T =
  let lightClient = LightClient(
    network: network,
    cfg: cfg,
    forkDigests: forkDigests,
    getBeaconTime: getBeaconTime,
    store: (ref ForkedLightClientStore)(),
  )

  func getTrustedBlockRoot(): Option[Eth2Digest] =
    # TODO: use Opt in LC processor
    if lightClient.trustedBlockRoot.isSome():
      some(lightClient.trustedBlockRoot.value)
    else:
      none(Eth2Digest)

  proc onStoreInitialized() =
    discard

  proc onFinalizedHeader() =
    if lightClient.onFinalizedHeader != nil:
      lightClient.onFinalizedHeader(lightClient, lightClient.getFinalizedHeader)

  proc onOptimisticHeader() =
    if lightClient.onOptimisticHeader != nil:
      lightClient.onOptimisticHeader(lightClient, lightClient.getOptimisticHeader)

  lightClient.processor = LightClientProcessor.new(
    dumpEnabled, dumpDirInvalid, dumpDirIncoming, cfg, genesis_validators_root,
    finalizationMode, lightClient.store, getBeaconTime, getTrustedBlockRoot,
    onStoreInitialized, onFinalizedHeader, onOptimisticHeader,
  )

  proc lightClientVerifier(
      obj: SomeForkedLightClientObject
  ): Future[Result[void, VerifierError]] {.
      async: (raises: [CancelledError], raw: true)
  .} =
    let resfut = Future[Result[void, VerifierError]].Raising([CancelledError]).init(
        "lightClientVerifier"
      )
    lightClient.processor[].addObject(MsgSource.gossip, obj, resfut)
    resfut

  proc bootstrapVerifier(obj: ForkedLightClientBootstrap): auto =
    lightClientVerifier(obj)

  proc updateVerifier(obj: ForkedLightClientUpdate): auto =
    lightClientVerifier(obj)

  proc finalityVerifier(obj: ForkedLightClientFinalityUpdate): auto =
    lightClientVerifier(obj)

  proc optimisticVerifier(obj: ForkedLightClientOptimisticUpdate): auto =
    lightClientVerifier(obj)

  func isLightClientStoreInitialized(): bool =
    lightClient.store[].kind > LightClientDataFork.None

  func isNextSyncCommitteeKnown(): bool =
    withForkyStore(lightClient.store[]):
      when lcDataFork > LightClientDataFork.None:
        forkyStore.is_next_sync_committee_known
      else:
        false

  func getFinalizedSlot(): Slot =
    withForkyStore(lightClient.store[]):
      when lcDataFork > LightClientDataFork.None:
        forkyStore.finalized_header.beacon.slot
      else:
        GENESIS_SLOT

  func getOptimisticSlot(): Slot =
    withForkyStore(lightClient.store[]):
      when lcDataFork > LightClientDataFork.None:
        forkyStore.optimistic_header.beacon.slot
      else:
        GENESIS_SLOT

  lightClient.manager = LightClientManager.init(
    lightClient.network, rng, getTrustedBlockRoot, bootstrapVerifier, updateVerifier,
    finalityVerifier, optimisticVerifier, isLightClientStoreInitialized,
    isNextSyncCommitteeKnown, getFinalizedSlot, getOptimisticSlot, getBeaconTime,
  )

  lightClient

proc new*(
    T: type LightClient,
    network: BeaconNetwork,
    rng: ref HmacDrbgContext,
    networkData: NetworkInitData,
    finalizationMode: LightClientFinalizationMode,
): T =
  let
    getBeaconTime = networkData.clock.getBeaconTimeFn()
    forkDigests = newClone networkData.forks

  LightClient.new(
    network,
    rng,
    dumpEnabled = false,
    dumpDirInvalid = ".",
    dumpDirIncoming = ".",
    networkData.metadata.cfg,
    forkDigests,
    getBeaconTime,
    networkData.genesis_validators_root,
    finalizationMode,
  )

proc start*(lightClient: LightClient) =
  notice "Starting beacon light client",
    trusted_block_root = lightClient.trustedBlockRoot
  lightClient.manager.start()

proc resetToFinalizedHeader*(
    lightClient: LightClient,
    header: ForkedLightClientHeader,
    current_sync_committee: altair.SyncCommittee,
) =
  lightClient.processor[].resetToFinalizedHeader(header, current_sync_committee)