const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
const { User, Rental, Item } = require("../models");
const PayoutService = require("./payoutService");
const logger = require("../utils/logger");
const { Op } = require("sequelize");
const { getPayoutFailureMessage } = require("../utils/payoutErrors");
const emailServices = require("./email");

class StripeWebhookService {
  /**
   * Verify webhook signature and construct event
   */
  static constructEvent(rawBody, signature, webhookSecret) {
    return stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);
  }

  /**
   * Handle account.updated webhook event.
   * Tracks requirements, triggers payouts when enabled, and notifies when disabled.
   * @param {Object} account - The Stripe account object from the webhook
   * @returns {Object} - { processed, payoutsTriggered, payoutResults, notificationSent }
   */
  static async handleAccountUpdated(account) {
    const accountId = account.id;
    const payoutsEnabled = account.payouts_enabled;
    const requirements = account.requirements || {};

    logger.info("Processing account.updated webhook", {
      accountId,
      payoutsEnabled,
      chargesEnabled: account.charges_enabled,
      detailsSubmitted: account.details_submitted,
      currentlyDue: requirements.currently_due?.length || 0,
      pastDue: requirements.past_due?.length || 0,
      disabledReason: requirements.disabled_reason,
    });

    // Find user with this Stripe account
    const user = await User.findOne({
      where: { stripeConnectedAccountId: accountId },
    });

    if (!user) {
      logger.warn("No user found for Stripe account", { accountId });
      return { processed: false, reason: "user_not_found" };
    }

    // Store previous state before update
    const previousPayoutsEnabled = user.stripePayoutsEnabled;

    // Update user with all account status fields
    await user.update({
      stripePayoutsEnabled: payoutsEnabled,
      stripeRequirementsCurrentlyDue: requirements.currently_due || [],
      stripeRequirementsPastDue: requirements.past_due || [],
      stripeDisabledReason: requirements.disabled_reason || null,
      stripeRequirementsLastUpdated: new Date(),
    });

    logger.info("Updated user Stripe account status", {
      userId: user.id,
      accountId,
      previousPayoutsEnabled,
      newPayoutsEnabled: payoutsEnabled,
      currentlyDue: requirements.currently_due?.length || 0,
      pastDue: requirements.past_due?.length || 0,
    });

    const result = {
      processed: true,
      payoutsTriggered: false,
      notificationSent: false,
    };

    // If payouts just became enabled (false -> true), process pending payouts
    if (payoutsEnabled && !previousPayoutsEnabled) {
      logger.info("Payouts enabled for user, processing pending payouts", {
        userId: user.id,
        accountId,
      });

      result.payoutsTriggered = true;
      result.payoutResults = await this.processPayoutsForOwner(user.id);
    }

    // If payouts just became disabled (true -> false), notify the owner
    if (!payoutsEnabled && previousPayoutsEnabled) {
      logger.warn("Payouts disabled for user", {
        userId: user.id,
        accountId,
        disabledReason: requirements.disabled_reason,
        currentlyDue: requirements.currently_due,
      });

      try {
        const disabledReason = this.formatDisabledReason(requirements.disabled_reason);

        await emailServices.payment.sendPayoutsDisabledEmail(user.email, {
          ownerName: user.firstName || user.lastName,
          disabledReason,
        });

        result.notificationSent = true;

        logger.info("Sent payouts disabled notification to owner", {
          userId: user.id,
          accountId,
          disabledReason: requirements.disabled_reason,
        });
      } catch (emailError) {
        logger.error("Failed to send payouts disabled notification", {
          userId: user.id,
          accountId,
          error: emailError.message,
        });
      }
    }

    return result;
  }

  /**
   * Format Stripe disabled_reason code to user-friendly message.
   * @param {string} reason - Stripe disabled_reason code
   * @returns {string} User-friendly message
   */
  static formatDisabledReason(reason) {
    const reasonMap = {
      "requirements.past_due":
        "Some required information is past due and must be provided to continue receiving payouts.",
      "requirements.pending_verification":
        "Your submitted information is being verified. This usually takes a few minutes.",
      listed: "Your account has been listed for review due to potential policy concerns.",
      platform_paused:
        "Payouts have been temporarily paused by the platform.",
      rejected_fraud: "Your account was flagged for potential fraudulent activity.",
      rejected_listed: "Your account has been rejected due to policy concerns.",
      rejected_terms_of_service:
        "Your account was rejected due to a terms of service violation.",
      rejected_other: "Your account was rejected. Please contact support for more information.",
      under_review: "Your account is under review. We'll notify you when the review is complete.",
    };

    return reasonMap[reason] || "Additional verification is required for your account.";
  }

  /**
   * Process all eligible payouts for a specific owner.
   * Called when owner completes Stripe onboarding.
   * @param {string} ownerId - The owner's user ID
   * @returns {Object} - { successful, failed, totalProcessed }
   */
  static async processPayoutsForOwner(ownerId) {
    const eligibleRentals = await Rental.findAll({
      where: {
        ownerId,
        status: "completed",
        paymentStatus: "paid",
        payoutStatus: "pending",
      },
      include: [
        {
          model: User,
          as: "owner",
          where: {
            stripeConnectedAccountId: { [Op.not]: null },
            stripePayoutsEnabled: true,
          },
        },
        { model: Item, as: "item" },
      ],
    });

    logger.info("Found eligible rentals for owner payout", {
      ownerId,
      count: eligibleRentals.length,
    });

    const results = {
      successful: [],
      failed: [],
      totalProcessed: eligibleRentals.length,
    };

    for (const rental of eligibleRentals) {
      try {
        const result = await PayoutService.processRentalPayout(rental);
        results.successful.push({
          rentalId: rental.id,
          amount: result.amount,
          transferId: result.transferId,
        });
      } catch (error) {
        results.failed.push({
          rentalId: rental.id,
          error: error.message,
        });
      }
    }

    logger.info("Processed payouts for owner", {
      ownerId,
      successful: results.successful.length,
      failed: results.failed.length,
    });

    return results;
  }

  /**
   * Handle payout.paid webhook event.
   * Updates rentals when funds are deposited to owner's bank account.
   * @param {Object} payout - The Stripe payout object
   * @param {string} connectedAccountId - The connected account ID (from event.account)
   * @returns {Object} - { processed, rentalsUpdated }
   */
  static async handlePayoutPaid(payout, connectedAccountId) {
    logger.info("Processing payout.paid webhook", {
      payoutId: payout.id,
      connectedAccountId,
      amount: payout.amount,
      arrivalDate: payout.arrival_date,
    });

    if (!connectedAccountId) {
      logger.warn("payout.paid webhook missing connected account ID", {
        payoutId: payout.id,
      });
      return { processed: false, reason: "missing_account_id" };
    }

    try {
      // Fetch balance transactions included in this payout
      // Filter by type 'transfer' to get only our platform transfers
      const balanceTransactions = await stripe.balanceTransactions.list(
        {
          payout: payout.id,
          type: "transfer",
          limit: 100,
        },
        { stripeAccount: connectedAccountId }
      );

      // Extract transfer IDs from balance transactions
      // The 'source' field contains the transfer ID
      const transferIds = balanceTransactions.data
        .map((bt) => bt.source)
        .filter(Boolean);

      if (transferIds.length === 0) {
        logger.info("No transfer balance transactions in payout", {
          payoutId: payout.id,
          connectedAccountId,
        });
        return { processed: true, rentalsUpdated: 0 };
      }

      logger.info("Found transfers in payout", {
        payoutId: payout.id,
        transferCount: transferIds.length,
        transferIds,
      });

      // Update all rentals with matching stripeTransferId
      const [updatedCount] = await Rental.update(
        {
          bankDepositStatus: "paid",
          bankDepositAt: new Date(payout.arrival_date * 1000),
          stripePayoutId: payout.id,
        },
        {
          where: {
            stripeTransferId: { [Op.in]: transferIds },
          },
        }
      );

      logger.info("Updated rentals with bank deposit status", {
        payoutId: payout.id,
        rentalsUpdated: updatedCount,
      });

      return { processed: true, rentalsUpdated: updatedCount };
    } catch (error) {
      logger.error("Error processing payout.paid webhook", {
        payoutId: payout.id,
        connectedAccountId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * Handle payout.failed webhook event.
   * Updates rentals when bank deposit fails and notifies the owner.
   * @param {Object} payout - The Stripe payout object
   * @param {string} connectedAccountId - The connected account ID (from event.account)
   * @returns {Object} - { processed, rentalsUpdated, notificationSent }
   */
  static async handlePayoutFailed(payout, connectedAccountId) {
    logger.info("Processing payout.failed webhook", {
      payoutId: payout.id,
      connectedAccountId,
      failureCode: payout.failure_code,
      failureMessage: payout.failure_message,
    });

    if (!connectedAccountId) {
      logger.warn("payout.failed webhook missing connected account ID", {
        payoutId: payout.id,
      });
      return { processed: false, reason: "missing_account_id" };
    }

    try {
      // Fetch balance transactions included in this payout
      const balanceTransactions = await stripe.balanceTransactions.list(
        {
          payout: payout.id,
          type: "transfer",
          limit: 100,
        },
        { stripeAccount: connectedAccountId }
      );

      const transferIds = balanceTransactions.data
        .map((bt) => bt.source)
        .filter(Boolean);

      if (transferIds.length === 0) {
        logger.info("No transfer balance transactions in failed payout", {
          payoutId: payout.id,
          connectedAccountId,
        });
        return { processed: true, rentalsUpdated: 0, notificationSent: false };
      }

      // Update all rentals with matching stripeTransferId
      const [updatedCount] = await Rental.update(
        {
          bankDepositStatus: "failed",
          stripePayoutId: payout.id,
          bankDepositFailureCode: payout.failure_code || "unknown",
        },
        {
          where: {
            stripeTransferId: { [Op.in]: transferIds },
          },
        }
      );

      logger.warn("Updated rentals with failed bank deposit status", {
        payoutId: payout.id,
        rentalsUpdated: updatedCount,
        failureCode: payout.failure_code,
      });

      // Find owner and send notification
      const user = await User.findOne({
        where: { stripeConnectedAccountId: connectedAccountId },
      });

      let notificationSent = false;

      if (user) {
        // Get user-friendly message
        const failureInfo = getPayoutFailureMessage(payout.failure_code);

        try {
          await emailServices.payment.sendPayoutFailedNotification(user.email, {
            ownerName: user.firstName || user.lastName,
            payoutAmount: payout.amount / 100,
            failureMessage: failureInfo.message,
            actionRequired: failureInfo.action,
            failureCode: payout.failure_code || "unknown",
            requiresBankUpdate: failureInfo.requiresBankUpdate,
          });

          notificationSent = true;

          logger.info("Sent payout failed notification to owner", {
            userId: user.id,
            payoutId: payout.id,
            failureCode: payout.failure_code,
          });
        } catch (emailError) {
          logger.error("Failed to send payout failed notification", {
            userId: user.id,
            payoutId: payout.id,
            error: emailError.message,
          });
        }
      } else {
        logger.warn("No user found for connected account", {
          connectedAccountId,
          payoutId: payout.id,
        });
      }

      return { processed: true, rentalsUpdated: updatedCount, notificationSent };
    } catch (error) {
      logger.error("Error processing payout.failed webhook", {
        payoutId: payout.id,
        connectedAccountId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * Handle payout.canceled webhook event.
   * Stripe can cancel payouts if:
   * - They are manually canceled via Dashboard/API before processing
   * - The connected account is deactivated
   * - Risk review cancels the payout
   * @param {Object} payout - The Stripe payout object
   * @param {string} connectedAccountId - The connected account ID
   * @returns {Object} - { processed, rentalsUpdated }
   */
  static async handlePayoutCanceled(payout, connectedAccountId) {
    logger.info("Processing payout.canceled webhook", {
      payoutId: payout.id,
      connectedAccountId,
    });

    if (!connectedAccountId) {
      logger.warn("payout.canceled webhook missing connected account ID", {
        payoutId: payout.id,
      });
      return { processed: false, reason: "missing_account_id" };
    }

    try {
      // Retrieve balance transactions to find associated transfers
      const balanceTransactions = await stripe.balanceTransactions.list(
        {
          payout: payout.id,
          type: "transfer",
          limit: 100,
        },
        { stripeAccount: connectedAccountId }
      );

      const transferIds = balanceTransactions.data
        .map((bt) => bt.source)
        .filter(Boolean);

      if (transferIds.length === 0) {
        logger.info("No transfers found for canceled payout", {
          payoutId: payout.id,
        });
        return { processed: true, rentalsUpdated: 0 };
      }

      // Update all rentals associated with this payout
      const [updatedCount] = await Rental.update(
        {
          bankDepositStatus: "canceled",
          stripePayoutId: payout.id,
        },
        {
          where: {
            stripeTransferId: { [Op.in]: transferIds },
          },
        }
      );

      logger.info("Updated rentals for canceled payout", {
        payoutId: payout.id,
        rentalsUpdated: updatedCount,
      });

      return { processed: true, rentalsUpdated: updatedCount };
    } catch (error) {
      logger.error("Error processing payout.canceled webhook", {
        payoutId: payout.id,
        connectedAccountId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * Handle account.application.deauthorized webhook event.
   * Triggered when an owner disconnects their Stripe account from our platform.
   * @param {string} accountId - The connected account ID that was deauthorized
   * @returns {Object} - { processed, userId, pendingPayoutsCount, notificationSent }
   */
  static async handleAccountDeauthorized(accountId) {
    logger.warn("Processing account.application.deauthorized webhook", {
      accountId,
    });

    if (!accountId) {
      logger.warn("account.application.deauthorized webhook missing account ID");
      return { processed: false, reason: "missing_account_id" };
    }

    try {
      // Find the user by their connected account ID
      const user = await User.findOne({
        where: { stripeConnectedAccountId: accountId },
      });

      if (!user) {
        logger.warn("No user found for deauthorized Stripe account", { accountId });
        return { processed: false, reason: "user_not_found" };
      }

      // Clear Stripe connection fields
      await user.update({
        stripeConnectedAccountId: null,
        stripePayoutsEnabled: false,
        stripeRequirementsCurrentlyDue: [],
        stripeRequirementsPastDue: [],
        stripeDisabledReason: null,
        stripeRequirementsLastUpdated: null,
      });

      logger.info("Cleared Stripe connection for deauthorized account", {
        userId: user.id,
        accountId,
      });

      // Check for pending payouts that will now fail
      const pendingRentals = await Rental.findAll({
        where: {
          ownerId: user.id,
          payoutStatus: "pending",
        },
      });

      if (pendingRentals.length > 0) {
        logger.warn("Owner disconnected account with pending payouts", {
          userId: user.id,
          pendingCount: pendingRentals.length,
          pendingRentalIds: pendingRentals.map((r) => r.id),
        });
      }

      // Send notification email
      let notificationSent = false;
      try {
        await emailServices.payment.sendAccountDisconnectedEmail(user.email, {
          ownerName: user.firstName || user.lastName,
          hasPendingPayouts: pendingRentals.length > 0,
          pendingPayoutCount: pendingRentals.length,
        });
        notificationSent = true;
        logger.info("Sent account disconnected notification", { userId: user.id });
      } catch (emailError) {
        logger.error("Failed to send account disconnected notification", {
          userId: user.id,
          error: emailError.message,
        });
      }

      return {
        processed: true,
        userId: user.id,
        pendingPayoutsCount: pendingRentals.length,
        notificationSent,
      };
    } catch (error) {
      logger.error("Error processing account.application.deauthorized webhook", {
        accountId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * Reconcile payout statuses for an owner by checking Stripe for actual status.
   * This handles cases where payout.paid, payout.failed, or payout.canceled webhooks were missed.
   *
   * Checks paid, failed, and canceled payouts to ensure accurate status tracking.
   *
   * @param {string} ownerId - The owner's user ID
   * @returns {Object} - { reconciled, updated, failed, notificationsSent, errors }
   */
  static async reconcilePayoutStatuses(ownerId) {
    const results = {
      reconciled: 0,
      updated: 0,
      failed: 0,
      notificationsSent: 0,
      errors: [],
    };

    try {
      // Find rentals that need reconciliation
      const rentalsToReconcile = await Rental.findAll({
        where: {
          ownerId,
          payoutStatus: "completed",
          stripeTransferId: { [Op.not]: null },
          bankDepositStatus: { [Op.is]: null },
        },
        include: [
          {
            model: User,
            as: "owner",
            attributes: ["id", "email", "firstName", "lastName", "stripeConnectedAccountId"],
          },
        ],
      });

      if (rentalsToReconcile.length === 0) {
        return results;
      }

      logger.info("Reconciling payout statuses", {
        ownerId,
        rentalsCount: rentalsToReconcile.length,
      });

      // Get the connected account ID (same for all rentals of this owner)
      const connectedAccountId = rentalsToReconcile[0].owner?.stripeConnectedAccountId;
      if (!connectedAccountId) {
        logger.warn("Owner has no connected account ID", { ownerId });
        return results;
      }

      // Fetch recent paid, failed, and canceled payouts
      const [paidPayouts, failedPayouts, canceledPayouts] = await Promise.all([
        stripe.payouts.list(
          { status: "paid", limit: 20 },
          { stripeAccount: connectedAccountId }
        ),
        stripe.payouts.list(
          { status: "failed", limit: 20 },
          { stripeAccount: connectedAccountId }
        ),
        stripe.payouts.list(
          { status: "canceled", limit: 20 },
          { stripeAccount: connectedAccountId }
        ),
      ]);

      // Build a map of transfer IDs to failed payouts for quick lookup
      const failedPayoutTransferMap = new Map();
      for (const payout of failedPayouts.data) {
        try {
          const balanceTransactions = await stripe.balanceTransactions.list(
            { payout: payout.id, type: "transfer", limit: 100 },
            { stripeAccount: connectedAccountId }
          );
          for (const bt of balanceTransactions.data) {
            if (bt.source) {
              failedPayoutTransferMap.set(bt.source, payout);
            }
          }
        } catch (btError) {
          logger.warn("Error fetching balance transactions for failed payout", {
            payoutId: payout.id,
            error: btError.message,
          });
        }
      }

      // Build a map of transfer IDs to canceled payouts for quick lookup
      const canceledPayoutTransferMap = new Map();
      for (const payout of canceledPayouts.data) {
        try {
          const balanceTransactions = await stripe.balanceTransactions.list(
            { payout: payout.id, type: "transfer", limit: 100 },
            { stripeAccount: connectedAccountId }
          );
          for (const bt of balanceTransactions.data) {
            if (bt.source) {
              canceledPayoutTransferMap.set(bt.source, payout);
            }
          }
        } catch (btError) {
          logger.warn("Error fetching balance transactions for canceled payout", {
            payoutId: payout.id,
            error: btError.message,
          });
        }
      }

      const owner = rentalsToReconcile[0].owner;

      for (const rental of rentalsToReconcile) {
        results.reconciled++;

        try {
          // First check if this transfer is in a failed payout
          const failedPayout = failedPayoutTransferMap.get(rental.stripeTransferId);

          if (failedPayout) {
            // Update rental with failed status
            await rental.update({
              bankDepositStatus: "failed",
              stripePayoutId: failedPayout.id,
              bankDepositFailureCode: failedPayout.failure_code || "unknown",
            });

            results.failed++;

            logger.warn("Reconciled rental with failed payout", {
              rentalId: rental.id,
              payoutId: failedPayout.id,
              failureCode: failedPayout.failure_code,
            });

            // Send failure notification
            if (owner?.email) {
              try {
                const failureInfo = getPayoutFailureMessage(failedPayout.failure_code);
                await emailServices.payment.sendPayoutFailedNotification(owner.email, {
                  ownerName: owner.firstName || owner.lastName,
                  payoutAmount: failedPayout.amount / 100,
                  failureMessage: failureInfo.message,
                  actionRequired: failureInfo.action,
                  failureCode: failedPayout.failure_code || "unknown",
                  requiresBankUpdate: failureInfo.requiresBankUpdate,
                });
                results.notificationsSent++;

                logger.info("Sent reconciled payout failure notification", {
                  userId: owner.id,
                  rentalId: rental.id,
                  payoutId: failedPayout.id,
                });
              } catch (emailError) {
                logger.error("Failed to send reconciled payout failure notification", {
                  userId: owner.id,
                  rentalId: rental.id,
                  error: emailError.message,
                });
              }
            }

            continue; // Move to next rental
          }

          // Check if this transfer is in a canceled payout
          const canceledPayout = canceledPayoutTransferMap.get(rental.stripeTransferId);

          if (canceledPayout) {
            await rental.update({
              bankDepositStatus: "canceled",
              stripePayoutId: canceledPayout.id,
            });

            results.canceled = (results.canceled || 0) + 1;

            logger.info("Reconciled rental with canceled payout", {
              rentalId: rental.id,
              payoutId: canceledPayout.id,
            });

            continue; // Move to next rental
          }

          // Check for paid payout
          const transfer = await stripe.transfers.retrieve(rental.stripeTransferId);
          const matchingPaidPayout = paidPayouts.data.find(
            (payout) => payout.arrival_date >= transfer.created
          );

          if (matchingPaidPayout) {
            await rental.update({
              bankDepositStatus: "paid",
              bankDepositAt: new Date(matchingPaidPayout.arrival_date * 1000),
              stripePayoutId: matchingPaidPayout.id,
            });

            results.updated++;

            logger.info("Reconciled rental payout status to paid", {
              rentalId: rental.id,
              payoutId: matchingPaidPayout.id,
              arrivalDate: matchingPaidPayout.arrival_date,
            });
          }
        } catch (rentalError) {
          results.errors.push({
            rentalId: rental.id,
            error: rentalError.message,
          });

          logger.error("Error reconciling rental payout status", {
            rentalId: rental.id,
            error: rentalError.message,
          });
        }
      }

      logger.info("Payout reconciliation complete", {
        ownerId,
        reconciled: results.reconciled,
        updated: results.updated,
        failed: results.failed,
        canceled: results.canceled || 0,
        notificationsSent: results.notificationsSent,
        errors: results.errors.length,
      });

      return results;
    } catch (error) {
      logger.error("Error in reconcilePayoutStatuses", {
        ownerId,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }
}

module.exports = StripeWebhookService;
