import pkg/questionable
import pkg/questionable/results
import pkg/metrics

import ../../logutils
import ../../market
import ../salesagent
import ../statemachine
import ./errorhandling
import ./cancelled
import ./failed
import ./filled
import ./ignored
import ./slotreserving
import ./errored

declareCounter(codex_reservations_availability_mismatch, "codex reservations availability_mismatch")

type
  SalePreparing* = ref object of ErrorHandlingState

logScope:
  topics = "marketplace sales preparing"

method `$`*(state: SalePreparing): string = "SalePreparing"

method onCancelled*(state: SalePreparing, request: StorageRequest): ?State =
  return some State(SaleCancelled())

method onFailed*(state: SalePreparing, request: StorageRequest): ?State =
  return some State(SaleFailed())

method onSlotFilled*(state: SalePreparing, requestId: RequestId,
                     slotIndex: UInt256): ?State =
  return some State(SaleFilled())

method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
  let agent = SalesAgent(machine)
  let data = agent.data
  let context = agent.context
  let market = context.market
  let reservations = context.reservations

  await agent.retrieveRequest()
  await agent.subscribe()

  without request =? data.request:
    raiseAssert "no sale request"

  let slotId = slotId(data.requestId, data.slotIndex)
  let state = await market.slotState(slotId)
  if state != SlotState.Free:
    return some State(SaleIgnored(reprocessSlot: false, returnBytes: false))

  # TODO: Once implemented, check to ensure the host is allowed to fill the slot,
  # due to the [sliding window mechanism](https://github.com/codex-storage/codex-research/blob/master/design/marketplace.md#dispersal)

  logScope:
    slotIndex = data.slotIndex
    slotSize = request.ask.slotSize
    duration = request.ask.duration
    pricePerSlot = request.ask.pricePerSlot

  # availability was checked for this slot when it entered the queue, however
  # check to the ensure that there is still availability as they may have
  # changed since being added (other slots may have been processed in that time)
  without availability =? await reservations.findAvailability(
      request.ask.slotSize,
      request.ask.duration,
      request.ask.pricePerSlot,
      request.ask.collateral):
    debug "No availability found for request, ignoring"

    return some State(SaleIgnored(reprocessSlot: true))

  info "Availability found for request, creating reservation"

  without reservation =? await reservations.createReservation(
    availability.id,
    request.ask.slotSize,
    request.id,
    data.slotIndex
  ), error:
    trace "Creation of reservation failed"
    # Race condition:
    # reservations.findAvailability (line 64) is no guarantee. You can never know for certain that the reservation can be created until after you have it.
    # Should createReservation fail because there's no space, we proceed to SaleIgnored.
    if error of BytesOutOfBoundsError:
       # Lets monitor how often this happen and if it is often we can make it more inteligent to handle it
      codex_reservations_availability_mismatch.inc()
      return some State(SaleIgnored(reprocessSlot: true))

    return some State(SaleErrored(error: error))

  trace "Reservation created succesfully"

  data.reservation = some reservation
  return some State(SaleSlotReserving())