Source: services/eventService.js

/**
 * @fileoverview Event Service for Satoshi Showdown.
 * Provides functionalities for managing event-related operations such as creating,
 * updating, deleting, and retrieving events. Also manages financial transactions
 * and webhooks associated with events. This service acts as a bridge between the
 * event controllers and the database models, ensuring that business logic and data
 * manipulation are handled effectively.
 *
 * @module services/eventService
 * @requires models/eventModel - Event data model for database interactions.
 * @requires services/walletService - Service for wallet management, crucial for financial transactions.
 * @requires services/transactionService - Service for creating and managing transaction records.
 * @requires services/webhookService - Service for managing webhooks, important for event notifications.
 * @requires services/userService - Service for user management, used for verifying user details.
 * @requires utils/validationUtil - Utility for data validation, ensures the integrity of input data.
 * @requires utils/errorUtil - Custom error classes and error handling utilities for consistent error management.
 * @requires utils/logUtil - Logging utility for application-wide logging, critical for monitoring and debugging.
 */

const Event = require("../models/eventModel");
const Wallet = require("../models/walletModel");
const {
  selectUTXOsForTransaction,
  selectUTXOsForAward,
  markUTXOAsSpent,
} = require("./utxoService");
const {
  createHDSegWitWalletForEvent,
  generateChildAddressForWallet,
  createRawBitcoinTransaction,
} = require("./walletService");
const {
  createTransactionRecord,
  createRefundTransactionRecord,
  getTransactionRecordById,
} = require("./transactionService");
const { createWebhook } = require("./webhookService");
const { getUserById } = require("./userService");
const { validateEvent } = require("../utils/validationUtil");
const {
  getCurrentFeeRates,
  estimateTransactionFee,
  adjustAmountForFee,
} = require("../utils/feeUtil");
const { ValidationError, NotFoundError } = require("../utils/errorUtil");
const log = require("../utils/logUtil");

/**
 * Creates a new event based on the provided user and event data, including financial setup.
 * This process involves validating the event data, setting up a wallet, creating a transaction record,
 * and saving the event to the database. The function returns both the created event and transaction details.
 *
 * @async
 * @function createEvent
 * @param {Object} eventData - Comprehensive data for creating the event.
 * @return {Promise<{event: Object, transaction: Object}>} An object containing the created event and transaction details.
 * @throws {ValidationError} If the event data does not pass validation.
 * @throws {NotFoundError} If the user is not found in the database.
 */
const createEvent = async (eventData) => {
  try {
    // Destructure all necessary fields from eventData
    const { userId, userAddress, prizePoolContribution, ...eventDetails } =
      eventData;

    // Fetch the user based on userId
    const user = await getUserById(userId);
    if (!user) {
      throw new NotFoundError(`User with ID ${userId} not found`);
    }

    // Set the event creator and calculate the total prize pool
    eventDetails.creator = user._id.toString();
    eventDetails.prizePool = eventDetails.entryFee + prizePoolContribution;

    // Validate the event details
    const validation = validateEvent(eventDetails);
    if (validation.error) {
      throw new ValidationError(
        `Invalid event data: ${validation.error.details
          .map((d) => d.message)
          .join("; ")}`,
      );
    }

    // Handle the financial setup for the event
    const financialSetup = await handleFinancialSetup(
      userAddress,
      user._id,
      eventDetails.entryFee,
      prizePoolContribution,
    );

    // Add transaction reference to event details
    eventDetails.transactions = [financialSetup.transaction._id];
    eventDetails.walletRef = financialSetup.wallet._id;

    // Create and save the new event
    const newEvent = new Event(eventDetails);
    // Automatically add event creator as a participant
    newEvent.participants.push({
      userId: user._id,
      depositAddress: financialSetup.wallet.addresses[0].address,
      userAddress,
      joinedAt: new Date(),
    });
    await newEvent.save();

    // Now create a webhook for the event
    const webhook = await createWebhook(
      financialSetup.wallet.addresses[0].address,
      financialSetup.wallet._id,
      financialSetup.transaction._id,
      user._id,
      newEvent._id,
    );

    log.info(`New event created with webhook: ${newEvent._id}`);
    return {
      event: newEvent,
      transaction: financialSetup.transaction,
      webhook: webhook,
    };
  } catch (err) {
    log.error(`Error in createEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Updates an existing event based on the provided event ID and update data.
 * This function looks up the event by its ID and applies the provided updates.
 * It ensures that the event data is kept current and accurate.
 *
 * @async
 * @function updateEvent
 * @param {string} eventId - ID of the event to be updated.
 * @param {Object} updateData - Data containing the updates to be applied to the event.
 * @param {string} updateData.name - Updated name of the event.
 * @param {string} updateData.description - Updated detailed description of the event.
 * @param {string} updateData.type - Updated type or category of the event.
 * @param {Date} updateData.startTime - Updated scheduled start time of the event.
 * @param {Date} updateData.endTime - Updated scheduled end time of the event.
 * @param {string} updateData.status - Updated current status of the event.
 * @param {number} updateData.entryFee - Updated entry fee required to participate in the event.
 * @param {number} updateData.prizePool - Updated total prize pool available for the event winners.
 * @param {ObjectID} updateData.creator - Updated reference to the User model for the event creator.
 * @param {Array} updateData.participants - Updated list of participants in the event.
 * @param {Array} updateData.transactions - Updated associated financial transactions.
 * @param {Array} updateData.winners - Updated list of winners of the event.
 * @param {Object} updateData.config - Updated custom configuration options for the event.
 * @param {string} updateData.streamingUrl - Updated URL for live streaming of the event.
 * @param {Object} updateData.streamingSchedule - Updated schedule for the streaming of the event.
 * @param {Array} updateData.bettingOptions - Updated betting options available for the event.
 * @param {number} updateData.viewCount - Updated number of views or attendance count for the event.
 * @param {Array} updateData.feedback - Updated user feedback associated with the event.
 * @param {Array} updateData.socialSharingLinks - Updated links for social sharing of the event.
 * @param {number} updateData.ageRestriction - Updated age restriction for participation or viewing.
 * @param {Array} updateData.geographicRestrictions - Updated geographic restrictions for the event.
 * @return {Promise<Object>} The updated event object, reflecting the changes made.
 * @throws {NotFoundError} When no event with the provided ID is found in the database.
 */
const updateEvent = async (eventId, updateData) => {
  try {
    const event = await Event.findByIdAndUpdate(eventId, updateData, {
      new: true,
    });
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }
    log.info(`Event updated: ${event._id}`);
    return event;
  } catch (err) {
    log.error(`Error in updateEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Adds a user to an event if there is space available, creates a transaction record,
 * and sets up a webhook for transaction monitoring.
 * Also handles optional prize pool contribution by the user.
 *
 * @async
 * @function joinEvent
 * @param {string} eventId - The unique identifier of the event to join.
 * @param {string} userId - The unique identifier (UUID) of the user attempting to join the event.
 * @param {number} [prizePoolContribution=0] - Optional contribution to the prize pool by the user.
 * @return {Promise<Object>} The updated event object reflecting the new participant and transaction.
 * @throws {NotFoundError} If the specified event or user is not found.
 * @throws {Error} If the event is already full or closed for new participants.
 */
const joinEvent = async (
  eventId,
  userId,
  userWalletAddress,
  prizePoolContribution = 0,
) => {
  try {
    // Find the event by its ID.
    const event = await Event.findOne({ eventId: eventId });
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    if (
      event.creator.toString() === userId ||
      event.participants.some(
        (participant) => participant.userId.toString() === userId,
      )
    ) {
      throw new Error(
        "Event creator or an already participating user cannot join the event",
      );
    }

    // Check if the event is open for new participants and not already full.
    if (!event.isOpen || event.participants.length >= event.maxParticipants) {
      throw new Error("Event is full or closed for new participants");
    }

    // Retrieve the user based on the provided userId.
    const user = await getUserById(userId);
    if (!user) {
      throw new NotFoundError(`User with ID ${userId} not found`);
    }

    // Retrieve the wallet
    const wallet = await Wallet.findById(event.walletRef);
    if (!wallet) {
      throw new NotFoundError(`Wallet with ID ${event.walletRef} not found`);
    }

    // Calculate total transaction amount (entry fee + optional prize pool contribution)
    const totalAmount = event.entryFee + prizePoolContribution;

    // Determine the next child index for the new address
    const childIndex = wallet.addresses.length;

    // Generate a new address based on the wallet
    const userDepositAddressData = await generateChildAddressForWallet(
      wallet.masterPublicKey,
      childIndex,
    );

    // Add the new address and path to the wallet
    wallet.addresses.push(userDepositAddressData);

    // Save the updated wallet
    await wallet.save();

    const transactionData = {
      userRef: user._id,
      walletRef: event.walletRef,
      transactionType: "incoming",
      expectedAmount: totalAmount,
      walletAddresses: [userDepositAddressData.address], // Add address to the first slot in the array      userAddress: userWalletAddress,
      userAddress: userWalletAddress,
      purpose:
        prizePoolContribution > 0 ? "payFeeAndFundPool" : "entryFeePayment",
    };

    const transaction = await createTransactionRecord(transactionData);

    // Update the prize pool if there's a contribution
    if (prizePoolContribution > 0) {
      event.prizePool += prizePoolContribution;
    }

    event.prizePool += event.entryFee;

    // Add the user to the event's participants list
    event.participants.push({
      userId: user._id,
      depositAddress: userDepositAddressData.address,
      userAddress: userWalletAddress,
      joinedAt: new Date(),
    });
    event.transactions.push(transaction._id);

    // Update event status to 'active' if it reaches the minimum number of participants.
    if (event.participants.length === event.minParticipants) {
      event.isOpen = false;
      event.status = "active";
    }

    // Save the updated event information to the database.
    await event.save();

    // Create a webhook for the transaction
    const webhook = await createWebhook(
      userDepositAddressData.address,
      wallet._id,
      transaction._id,
      user._id,
      event._id,
    );

    // Return the updated event object including transaction details.
    return {
      event: event,
      transaction: transaction,
      webhook: webhook,
    };
  } catch (err) {
    log.error(`Error in joinEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Retrieves a specific event by its unique ID.
 * This function is used to get detailed information about a single event,
 * including its data and related transactions or webhooks.
 *
 * @async
 * @function getEvent
 * @param {string} eventId - ID of the event to be retrieved.
 * @return {Promise<Object>} The event object corresponding to the specified ID.
 * @throws {NotFoundError} When the event with the given ID does not exist in the database.
 */
const getEvent = async (eventId) => {
  try {
    const event = await Event.findById(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }
    return event;
  } catch (err) {
    log.error(`Error in getEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Retrieves all events available in the system.
 * This function is typically used to list all events, for example, in an administrative
 * dashboard or a public event listing. It provides a complete overview of all events.
 *
 * @async
 * @function getAllEvents
 * @return {Promise<Array>} An array containing all event objects in the database.
 */
const getAllEvents = async () => {
  try {
    return await Event.find({});
  } catch (err) {
    log.error(`Error in getAllEvents: ${err.message}`);
    throw err;
  }
};

/**
 * Deletes an event from the system based on its ID.
 * This function removes the event and any associated data from the database,
 * effectively canceling the event and cleaning up resources.
 *
 * @async
 * @function deleteEvent
 * @param {string} eventId - ID of the event to be deleted.
 * @return {Promise<void>} Indicates successful deletion of the event.
 * @throws {NotFoundError} When the event with the specified ID is not found.
 */
const deleteEvent = async (eventId) => {
  try {
    const event = await Event.findByIdAndRemove(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }
    log.info(`Event deleted: ${event._id}`);
  } catch (err) {
    log.error(`Error in deleteEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Handles the financial setup for an event, including wallet and transaction creation.
 * This private function is used by `createEvent` to encapsulate the financial aspects of event setup.
 * It ensures a smooth and error-free process by creating a wallet and transaction for the event.
 *
 * @async
 * @private
 * @function handleFinancialSetup
 * @param {string} userAddress - Bitcoin address for the transaction.
 * @param {string} userRef - User reference ID.
 * @param {number} entryFee - Entry fee required to participate in the event.
 * @param {number} prizePoolContribution - Contribution to the prize pool, if any.
 * @return {Promise<Object>} An object containing details about the wallet and transaction created.
 * @throws {Error} When financial setup encounters an error, such as wallet creation failure.
 */
const handleFinancialSetup = async (
  userAddress,
  userRef,
  entryFee,
  prizePoolContribution,
) => {
  try {
    // Calculate the total amount the user needs to send in
    const totalAmount = entryFee + prizePoolContribution;
    let transactionPurpose;

    if (prizePoolContribution > 0) {
      transactionPurpose = "payFeeAndFundPool";
    } else {
      transactionPurpose = "entryFeePayment";
    }

    // MOVE THIS TO A SEPARATE FUNCTION
    // Generate a unique random amount of satoshis
    const uniqueSatoshis = Math.floor(Math.random() * 1000); // Adjust the range as needed
    // Add the unique amount to the total
    const expectedAmount = totalAmount + uniqueSatoshis;
    // END OF MOVE THIS TO A SEPARATE FUNCTION

    // Create a SegWit wallet for the event
    const wallet = await createHDSegWitWalletForEvent();

    // Prepare transaction data for the financial setup
    const transactionData = {
      userRef: userRef,
      walletRef: wallet._id,
      transactionType: "incoming",
      expectedAmount: expectedAmount,
      unconfirmedAmount: expectedAmount,
      walletAddresses: [wallet.addresses[0].address],
      userAddress: userAddress,
      purpose: transactionPurpose,
    };

    // Create a transaction record for the event
    const transaction = await createTransactionRecord(transactionData);

    log.info(
      `Financial setup completed for event: Wallet and transaction created`,
    );

    // Return an object containing wallet and transaction details
    return { wallet, transaction };
  } catch (err) {
    log.error(`Error in handleFinancialSetup: ${err.message}`);
    throw new Error(
      `Failed to set up financial aspects of the event: ${err.message}`,
    );
  }
};

/**
 * Refunds the creator of an event.
 * @async
 * @function refundEventCreator
 * @param {string} eventId - ID of the event to refund.
 * @return {Promise<Object>} Details of the refund transaction.
 * @throws {Error} If the refund process encounters an issue.
 */
const refundEventCreator = async (eventId) => {
  try {
    const event = await Event.findById(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    // Retrieve the original transaction record
    const originalTransactionId = event.transactions[0];
    const originalTransaction = await getTransactionRecordById(
      originalTransactionId,
    );

    const refundAmount = originalTransaction.confirmedAmount; // Assuming full refund
    const selectedUTXOs = await selectUTXOsForTransaction(
      event.creator,
      eventId,
      refundAmount,
    );

    // Fetch current fee rates and estimate transaction fee
    const feeRates = await getCurrentFeeRates();
    const feeRate = feeRates.lowFeePerByte;
    const estimatedFee = estimateTransactionFee(
      selectedUTXOs.length,
      1,
      feeRate,
    ); // Assuming 1 output

    console.log(feeRates);
    console.log(feeRate);
    console.log(estimatedFee);

    const adjustedRefundAmount = adjustAmountForFee(refundAmount, estimatedFee);

    // Prepare the refund transaction data
    const refundTransactionData = {
      userRef: originalTransaction.userRef,
      walletRef: originalTransaction.walletRef,
      transactionType: "outgoing",
      purpose: "refundUser",
      walletAddress: originalTransaction.walletAddress,
      userAddress: originalTransaction.userAddress,
      expectedAmount: adjustedRefundAmount,
      unconfirmedAmount: adjustedRefundAmount,
      confirmedAmount: 0,
      status: "pending",
      transactionHash: null,
    };

    const refundTransaction = await createRefundTransactionRecord(
      refundTransactionData,
    );

    event.status = "cancelled";
    event.transactions.push(refundTransaction._id);
    event.prizePool = 0;
    event.isOpen = false;
    event.closedAt = new Date();

    await event.save();

    // await createWebhook(refundTransaction.walletAddress, refundTransaction._id, event.creator, event._id);

    for (const utxo of selectedUTXOs) {
      await markUTXOAsSpent(utxo.transactionHash, utxo.outputIndex);
    }

    // Specify a change address - ideally, this should be a new address generated for the event or user
    const changeAddress =
      event.walletAddress || originalTransaction.walletAddress;

    const rawTransaction = await createRawBitcoinTransaction(
      selectedUTXOs,
      refundTransaction.userAddress,
      refundAmount - estimatedFee,
      changeAddress,
      estimatedFee,
    );

    console.log(rawTransaction);

    log.info(`Refund processed for event: ${eventId}`);
    return refundTransaction;
  } catch (err) {
    log.error(`Error in refundEventCreator: ${err.message}`);
    throw err;
  }
};

/**
 * Refunds a user associated with an event or a specific transaction.
 * @async
 * @function refundUser
 * @param {string} userId - ID of the user to refund.
 * @param {string} eventId - ID of the event associated with the refund.
 * @param {number} [refundAmount] - Amount to refund. If not provided, full amount is refunded.
 * @return {Promise<Object>} Details of the refund transaction.
 * @throws {Error} If the refund process encounters an issue.
 */
const refundUser = async (userId, eventId, refundAmount = null) => {
  try {
    const event = await Event.findById(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    // Determine the transaction to refund
    let transactionToRefund;
    for (const transactionId of event.transactions) {
      const transaction = await getTransactionRecordById(transactionId);
      if (transaction.userRef.toString() === userId.toString()) {
        transactionToRefund = transaction;
        break;
      }
    }

    if (!transactionToRefund) {
      throw new Error(
        `No transaction found for user ID ${userId} in event ID ${eventId}`,
      );
    }

    // If refundAmount not specified, refund the full transaction amount
    refundAmount = refundAmount || transactionToRefund.confirmedAmount;

    // Select UTXOs for the refund transaction
    const selectedUTXOs = await selectUTXOsForTransaction(
      userId,
      eventId,
      refundAmount,
    );

    // Estimate transaction fee and adjust refund amount
    const feeRates = await getCurrentFeeRates();
    const estimatedFee = estimateTransactionFee(
      selectedUTXOs.length,
      1,
      feeRates.lowFeePerByte,
    );
    const adjustedRefundAmount = adjustAmountForFee(refundAmount, estimatedFee);

    // Prepare the refund transaction data
    const refundTransactionData = {
      userRef: userId,
      walletRef: transactionToRefund.walletRef,
      transactionType: "outgoing",
      purpose: "refundUser",
      walletAddress: transactionToRefund.walletAddress,
      userAddress: transactionToRefund.userAddress,
      expectedAmount: adjustedRefundAmount,
      unconfirmedAmount: adjustedRefundAmount,
      confirmedAmount: 0,
      status: "pending",
      transactionHash: null,
    };

    const refundTransaction = await createRefundTransactionRecord(
      refundTransactionData,
    );

    // Update the event's transactions to include the refund transaction
    event.transactions.push(refundTransaction._id);
    await event.save();

    // Specify a change address - ideally, this should be a new address generated for the user or event
    const changeAddress =
      event.walletAddress || transactionToRefund.walletAddress;

    // Create the raw Bitcoin transaction
    const rawTransaction = await createRawBitcoinTransaction(
      selectedUTXOs,
      refundTransaction.userAddress,
      adjustedRefundAmount - estimatedFee,
      changeAddress,
      estimatedFee,
    );

    log.info(`Refund processed for user ${userId} in event ${eventId}`);
    return { refundTransaction, rawTransaction };
  } catch (err) {
    log.error("Error in refundUser: ${err.message}");
    throw err;
  }
};

/**
 * Settles an event based on participant votes.
 * @param {string} eventId - ID of the event to settle.
 */
const settleEvent = async (eventId) => {
  try {
    const event = await Event.findById(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    if (event.status === "active" && new Date() > event.endTime) {
      event.status = "settling";
      event.settlementStatus = "voting";
      await event.save();
      log.info(`Event ${eventId} status updated to settling`);
    } else {
      throw new Error(
        "Event is not in a state to be settled or end time not reached",
      );
    }
  } catch (err) {
    log.error(`Error in settleEvent: ${err.message}`);
    throw err;
  }
};

/**
 * Casts a vote for an event participant.
 *
 * @async
 * @function castVote
 * @param {string} eventId - The event's unique identifier.
 * @param {string} userId - The user's unique identifier.
 * @param {string} vote - The vote cast by the user.
 * @return {Promise<Object>} Confirmation message.
 * @throws {NotFoundError} If the event or user is not found.
 * @throws {Error} If voting is not open or if invalid voting attempt.
 */
const castVote = async (eventId, userId, vote) => {
  try {
    const event = await Event.findOne({ eventId: eventId });
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    if (event.status !== "settling" && event.settlementStatus !== "voting") {
      throw new Error("Voting is not open for this event");
    }

    const user = await getUserById(userId);
    if (!user) {
      throw new NotFoundError(`User with ID ${userId} not found`);
    }

    const isParticipant = event.participants.some(
      (p) => p.userId.toString() === user._id.toString(),
    );
    const hasVoted = event.voteResults.some(
      (v) => v.userId.toString() === user._id.toString(),
    );

    if (!isParticipant || hasVoted) {
      throw new Error("Invalid voting attempt");
    }

    event.voteResults.push({ userId: user._id, vote, timestamp: new Date() });
    await event.save();

    log.info(`Vote recorded for user ${userId} in event ${eventId}`);
    return { message: "Vote cast successfully" };
  } catch (err) {
    log.error(`Error in castVote: ${err.message}`);
    throw err;
  }
};

/**
 * Determines the outcome of an event based on participant votes.
 * Retrieves the event from the database, checks the vote results against the number of participants,
 * and updates the event document accordingly.
 *
 * @async
 * @function determineOutcome
 * @param {string} eventId - The ID of the event to determine the outcome for.
 * @return {Promise<Object>} An object representing the outcome of the event.
 * @throws {Error} If the event is not found or if there's an error in processing the votes.
 */
const determineOutcome = async (eventId) => {
  try {
    // Retrieve the event from the database
    const event = await Event.findById(eventId);
    if (!event) {
      throw new Error(`Event with ID ${eventId} not found`);
    }

    // Ensure the number of votes is equal to the number of participants
    if (event.voteResults.length !== event.participants.length) {
      throw new Error("Not all participants have voted");
    }

    // Extract votes from the event document
    const { voteResults } = event;
    const voteCount = voteResults.reduce((acc, vote) => {
      acc[vote.vote] = (acc[vote.vote] || 0) + 1;
      return acc;
    }, {});

    // Determine outcome based on votes
    if (voteCount.win === 1 && voteCount.loss === 1) {
      const winnerVote = voteResults.find((v) => v.vote === "win");
      event.winners = [winnerVote.userId];
      event.isDisputed = false;
      event.settlementStatus = "processing";
      await event.save(); // Save the updated event
      return { winner: winnerVote.userId, isDisputed: false };
    } else if (voteCount.draw === event.participants.length) {
      event.isDisputed = false;
      await event.save(); // Save the updated event
      return { isDraw: true, isDisputed: false };
    } else {
      event.isDisputed = true;
      await event.save(); // Save the updated event
      // Any other combination is considered a dispute
      return { isDisputed: true };
    }
  } catch (err) {
    throw new Error(`Error in determineOutcome: ${err.message}`);
  }
};

/**
 * Awards the prize to the winner of an event. This function is called after an event
 * has been settled and a winner has been determined. It retrieves the event's UTXOs,
 * calculates the total prize amount, creates a raw Bitcoin transaction for prize distribution,
 * and updates the event status to 'completed'.
 *
 * The function fetches the winner's user address from the participants array in the event document
 * using the winner's user ID stored in the winners field. It ensures that the event is in the
 * appropriate state for awarding the prize and that a winner is present. The prize is then
 * sent to the winner's user address.
 *
 * @async
 * @function awardWinner
 * @param {string} eventId - The unique identifier of the event whose winner is to be awarded.
 * @return {Promise<Object>} An object containing details of the prize distribution transaction and the raw transaction.
 * @throws {NotFoundError} If the event or the winner is not found.
 * @throws {Error} If the event is not in the correct state to award the winner or if there are issues in creating the transaction.
 */
const awardWinner = async (eventId) => {
  try {
    const event = await Event.findById(eventId);
    if (!event) {
      throw new NotFoundError(`Event with ID ${eventId} not found`);
    }

    if (
      event.status !== "settling" ||
      event.settlementStatus !== "processing"
    ) {
      throw new Error("Event is not in a state to award the winner");
    }

    if (event.winners.length === 0) {
      throw new Error("No winners to award in this event");
    }

    const winnerId = event.winners[0];
    const winnerParticipant = event.participants.find((p) =>
      p.userId.equals(winnerId),
    );
    if (!winnerParticipant) {
      throw new NotFoundError(
        `Winner with ID ${winnerId} not found in participants`,
      );
    }

    // Since the UTXOs now directly reflect the prize pool, we don't need to pass the required amount
    const selectedUTXOs = await selectUTXOsForAward(eventId);

    // Estimate transaction fee
    const testnetFeeRate = 22; // Assuming testnet usage
    const estimatedFee = estimateTransactionFee(
      selectedUTXOs.length,
      1,
      testnetFeeRate,
    );

    console.log("estimatedFee:", estimatedFee);

    // Adjust prize amount after fee
    const prizeAmountAfterFee = event.prizePool - estimatedFee;

    // Create the raw Bitcoin transaction
    const rawTransaction = await createRawBitcoinTransaction(
      selectedUTXOs,
      winnerParticipant.userAddress,
      prizeAmountAfterFee,
      winnerParticipant.userAddress, // If there's change just send to user for now
      estimatedFee,
    );

    const prizeDistributionTransaction = {
      userRef: winnerId,
      walletRef: event.walletRef,
      transactionType: "outgoing",
      purpose: "winnerPayout",
      walletAddresses: selectedUTXOs.map((utxo) => utxo.address),
      userAddress: winnerParticipant.userAddress,
      expectedAmount: prizeAmountAfterFee,
      unconfirmedAmount: prizeAmountAfterFee,
      confirmedAmount: 0,
      status: "pending",
      transactionHash: null,
    };

    await createTransactionRecord(prizeDistributionTransaction);

    // Mark the used UTXOs as spent
    // for (const utxo of selectedUTXOs) {
    //   await markUTXOAsSpent(utxo.transactionHash, utxo.outputIndex);
    // }

    // event.status = "completed";
    // event.settlementStatus = "settled";
    // await event.save();

    log.info(`Prize awarded to winner for event ${eventId}`);
    return { transaction: prizeDistributionTransaction, rawTransaction };
  } catch (err) {
    log.error(`Error in awardWinner: ${err.message}`);
    throw err;
  }
};

module.exports = {
  createEvent,
  updateEvent,
  joinEvent,
  getEvent,
  getAllEvents,
  deleteEvent,
  refundUser,
  refundEventCreator,
  settleEvent,
  castVote,
  determineOutcome,
  awardWinner,
};