/**
   * Commit seats held for a specific customer
   *
   * @param seatHoldId the seat hold identifier
   * @param customerEmail the email address of the customer to which the seat hold is assigned
   * @return a reservation confirmation code
   */
  @PUT
  @Timed
  @ExceptionMetered
  @Path("/reserveSeats")
  @Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON})
  public String reserveSeats(
      @QueryParam("seatHoldId") @NotNull Integer seatHoldId, @Email @NotBlank String customerEmail)
      throws InvalidSeatHoldRequestException, ReservationNotFoundException {
    validateCustomerEmail(customerEmail);

    Integer seatLevelId = holdId2SeatLevel.get(seatHoldId);
    if (seatLevelId == null) {
      throw new InvalidSeatHoldRequestException(
          String.format("Seat hold #%d does not exist in the venue", seatHoldId));
    }

    ReservationManager resource = seatLevelResourceManager[seatLevelId];
    ReservationHold hold = resource.getReservation(seatHoldId);
    if (hold == null) {
      throw new ReservationNotFoundException(seatHoldId, customerEmail);
    }

    SeatHold seatHold = buildSeatHold(hold);
    if (!Objects.equals(seatHold.getCustomerEmail(), customerEmail)) {
      throw new InvalidSeatHoldRequestException(
          String.format(
              "Seat hold #%d is not associated with customer email %s", seatHoldId, customerEmail));
    }

    if (!resource.confirmHold(seatHold.getReservationId())) {
      throw new ReservationNotFoundException(seatHoldId, customerEmail);
    }

    return generateReservationCode(seatHoldId, customerEmail);
  }
  /**
   * The number of seats in the requested level that are neither held nor reserved
   *
   * @param venueLevel a numeric venue level identifier to limit the search
   * @return the number of tickets available on the provided level
   * @throws VenueLevelNotFoundException
   */
  @GET
  @Timed
  @ExceptionMetered
  @Path("/numSeatsAvailable")
  @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
  public int numSeatsAvailable(@QueryParam("venueLevel") Integer venueLevel)
      throws VenueLevelNotFoundException {
    int fromLevel = 0;
    int toLevel = seatLevelResourceManager.length - 1;

    if (venueLevel != null) {
      if (venueLevel < 0 || venueLevel >= seatLevelResourceManager.length) {
        throw new VenueLevelNotFoundException(venueLevel);
      }

      fromLevel = venueLevel;
      toLevel = venueLevel;
    }

    int available = 0;
    for (; fromLevel <= toLevel; ++fromLevel) {
      ReservationManager resource = seatLevelResourceManager[fromLevel];
      available += resource.countAvailableSlots();
    }

    return available;
  }
  /**
   * Find and hold the best available seats for a customer
   *
   * @param numSeats the number of seats to find and hold
   * @param minLevel the minimum venue level
   * @param maxLevel the maximum venue level
   * @param customerEmail unique identifier for the customer
   * @return a SeatHold object identifying the specific seats and related information
   * @throws InvalidSeatHoldRequestException, NoSeatsAvailableException
   */
  @POST
  @Timed
  @ExceptionMetered
  @Path("/findAndHoldSeats")
  public SeatHold findAndHoldSeats(
      @QueryParam("numSeats") @NotNull Integer numSeats,
      @QueryParam("minLevel") Integer minLevel,
      @QueryParam("maxLevel") Integer maxLevel,
      @Email @NotNull @NotBlank String customerEmail)
      throws InvalidSeatHoldRequestException, NoSeatsAvailableException {
    if (numSeats == 0) {
      return null;
    }

    if (numSeats < 0) {
      throw new InvalidSeatHoldRequestException(
          String.format("Invalid number of hold [%d] being requested", numSeats));
    }

    int lo = minLevel != null ? minLevel : 0;
    int hi = maxLevel != null ? maxLevel : seatLevelResourceManager.length - 1;

    if (lo > hi || lo < 0 || hi >= seatLevelResourceManager.length) {
      throw new InvalidSeatHoldRequestException(
          String.format("Seat hold level constraints are not valid [%d, %d]", lo, hi));
    }

    validateCustomerEmail(customerEmail);

    for (; hi >= lo; --hi) {
      ReservationManager resource = seatLevelResourceManager[hi];
      ReservationHold hold = resource.requestHold(numSeats, customerEmail);

      if (hold != null) {
        SeatHold seatHold = buildSeatHold(hold);
        holdId2SeatLevel.put(seatHold.getReservationId(), seatHold.getLevel());

        return seatHold;
      }
    }

    throw new NoSeatsAvailableException();
  }
  /**
   * The details for reservation holds at the venue
   *
   * @return
   * @throws VenueLevelNotFoundException
   */
  @GET
  @Timed
  @ExceptionMetered
  @Path("/holdDetails")
  public List<SeatLevelHoldDetail> venueHoldDetails(@QueryParam("venueLevel") Integer venueLevel)
      throws VenueLevelNotFoundException {
    ArrayList<SeatLevelHoldDetail> seatLevelHoldDetails = new ArrayList<>();

    int fromLevel = 0;
    int toLevel = seatLevelResourceManager.length - 1;

    if (venueLevel != null) {
      if (venueLevel < 0 || venueLevel >= seatLevelResourceManager.length) {
        throw new VenueLevelNotFoundException(venueLevel);
      }

      fromLevel = venueLevel;
      toLevel = venueLevel;
    }

    for (; fromLevel <= toLevel; ++fromLevel) {
      ReservationManager resource = seatLevelResourceManager[fromLevel];

      SeatLevel seatLevel = configuration.getSeatLevels().get(fromLevel);

      List<SeatHold> seatHolds =
          resource
              .getAllReservations()
              .stream()
              .map(this::buildSeatHold)
              .collect(Collectors.toList());

      seatLevelHoldDetails.add(
          new SeatLevelHoldDetail(
              seatLevel.getName(), seatLevel.getRows(), seatLevel.getSeatsInRow(), seatHolds));
    }

    return seatLevelHoldDetails;
  }
  /**
   * Initialize the venue seat reservation manager
   *
   * @param configuration
   * @throws Exception
   */
  public VenueTicketManager(VenueSeatingConfiguration configuration) throws Exception {
    if (configuration.getSeatLevels().size() == 0) {
      throw new IllegalArgumentException("configuration.seatLevels");
    }

    this.configuration = configuration;
    this.seatLevelResourceManager = new ReservationManager[configuration.getSeatLevels().size()];

    for (int lvlNum = 0; lvlNum < configuration.getSeatLevels().size(); ++lvlNum) {
      SeatLevel lvl = configuration.getSeatLevels().get(lvlNum);
      if (lvl.getRows() < 1) {
        throw new Exception(
            String.format(
                "Invalid number of rows (%d) for seating level %d", lvl.getRows(), lvlNum));
      }

      if (lvl.getSeatsInRow() < 1) {
        throw new Exception(
            String.format(
                "Invalid number of seats (%d) for seating level %d, row %d",
                lvlNum, lvl.getRows(), lvl.getSeatsInRow()));
      }

      int capacity = lvl.getRows() * lvl.getSeatsInRow();

      ReservationManager resource =
          new ReservationManager(
              lvlNum,
              capacity,
              new HoldTimeStampProvider(configuration.getHoldsExpireAfter()),
              lvl.getName());

      resource.setReservationIdCounter(reservationIdCounter);

      this.seatLevelResourceManager[lvlNum] = resource;
    }
  }