import db from "../models/index.js";
import { Op } from "sequelize";
import RateService from "./rateService.js";
import { sendEmail } from "../utils/emailService.js";
import { baseEmailLayout } from "../utils/emailTemplates/baseLayout.js";
import DiditService from "../thirdParties/diditService.js";
import { applyDiditKybDecision } from "./kybDecisionService.js";

const { Merchant, Customer, ExchangeRate, MerchantSubscription, SubscriptionPlan, SubscriptionInvoice, KybApplication, Currency, MerchantWalletBalance, sequelize } = db;

/**
 * Reconciles merchant currency wallets against each Currency's virtualAccountStatus.
 * For currencies enabled for virtual accounts, every merchant gets (or has re-activated)
 * a wallet row. For currencies disabled, any existing wallet rows for that currency are
 * marked inactive — never deleted, so balances and history are preserved. NGN is always
 * skipped: it lives on Merchant.balance directly, not in this table.
 * Runs once at server startup and then daily.
 */
const handleCronError = (taskName, error) => {
    if (error?.name === "SequelizeConnectionRefusedError" || error?.original?.code === "ECONNREFUSED") {
        console.warn(`⚠️ [CRON] ${taskName} Skipped: MySQL database connection refused (MySQL offline).`);
    } else {
        console.error(`❌ [CRON] ${taskName} Error:`, error.message || error);
    }
};

export const syncMerchantWalletCurrencies = async () => {
    try {
        console.log("⏰ [CRON] Running Merchant Wallet Currency Sync...");

        const merchants = await Merchant.findAll({ attributes: ["id"] });
        const merchantIds = merchants.map((m) => m.id);

        const currencies = await Currency.findAll({
            where: { code: { [Op.ne]: "NGN" } },
        });

        let created = 0;
        let activated = 0;
        let deactivated = 0;

        for (const currency of currencies) {
            if (currency.virtualAccountStatus === "active") {
                for (const merchantId of merchantIds) {
                    try {
                        const [wallet, wasCreated] = await MerchantWalletBalance.findOrCreate({
                            where: { merchantId, currency: currency.code },
                            defaults: { balance: 0.00, prev_balance: 0.00, status: "active" },
                        });
                        if (wasCreated) {
                            created++;
                        } else if (wallet.status !== "active") {
                            await wallet.update({ status: "active" });
                            activated++;
                        }
                    } catch (err) {
                        console.error(`❌ [CRON] Failed to sync wallet for merchant ${merchantId} / ${currency.code}:`, err.message);
                    }
                }
            } else {
                const [count] = await MerchantWalletBalance.update(
                    { status: "inactive" },
                    { where: { currency: currency.code, status: "active" } }
                );
                deactivated += count;
            }
        }

        console.log(`✅ [CRON] Merchant Wallet Currency Sync Complete. Created ${created}, activated ${activated}, deactivated ${deactivated}.`);
    } catch (error) {
        handleCronError("Merchant Wallet Currency Sync", error);
    }
};

/**
 * Polls Didit for the current decision on every KYB application that's still
 * in-flight (submitted to Didit but not yet in a terminal state). Acts as a
 * fallback for merchants whose status.updated webhook never arrived or failed
 * signature verification — the webhook stays the primary, fast path; this just
 * makes sure nobody gets permanently stuck.
 */
export const syncPendingKybApplications = async () => {
    try {
        console.log("⏰ [CRON] Running Pending KYB Sync...");

        const pending = await KybApplication.findAll({
            where: {
                status: { [Op.in]: ["Submitted", "Under Review", "Additional Documents Required"] },
                diditSessionId: { [Op.ne]: null },
            },
            include: [{ model: Merchant, as: "merchant" }],
        });

        let updated = 0;
        let failed = 0;

        for (const application of pending) {
            try {
                const decision = await DiditService.getSessionDecision(application.diditSessionId);
                const result = await applyDiditKybDecision(application, decision, "cron");
                if (result.changed) updated++;
            } catch (err) {
                failed++;
                console.error(`❌ [CRON] Failed to sync KYB application ${application.id} (session ${application.diditSessionId}):`, err.message);
            }
        }

        if (pending.length > 0) {
            console.log(`✅ [CRON] Pending KYB Sync Complete. Checked ${pending.length}, updated ${updated}, failed ${failed}.`);
        }
    } catch (error) {
        handleCronError("Pending KYB Sync", error);
    }
};

/**
 * Checks for Merchants and Customers who have been inactive for more than 24 hours
 * and sets their status to 'inactive'.
 */
export const checkInactivity = async () => {
    try {
        console.log("⏰ [CRON] Running Inactivity Check...");
        
        const oneDayAgo = new Date();
        oneDayAgo.setHours(oneDayAgo.getHours() - 24);

        // 1. Deactivate Merchants who haven't performed any action in 24h
        const [mCount] = await Merchant.update(
            { status: "inactive" },
            {
                where: {
                    status: "active",
                    lastActivityAt: { [Op.lt]: oneDayAgo }
                }
            }
        );

        // 2. Deactivate Customers who haven't had any activity in 24h
        const [cCount] = await Customer.update(
            { status: "inactive" },
            {
                where: {
                    status: "active",
                    lastActivityAt: { [Op.lt]: oneDayAgo }
                }
            }
        );

        if (mCount > 0 || cCount > 0) {
            console.log(`✅ [CRON] Inactivity Check Complete. Deactivated ${mCount} merchants and ${cCount} customers.`);
        }
    } catch (error) {
        handleCronError("Inactivity Check", error);
    }
};

/**
 * Syncs exchange rates from their configured third-party sources (e.g. WeWire, Graph)
 * and updates them in the database.
 */
export const syncExchangeRates = async () => {
    try {
        console.log("⏰ [CRON] Running Exchange Rate Sync...");
        
        const rates = await ExchangeRate.findAll();
        
        for (const exchangeRate of rates) {
            if (exchangeRate.rateSource === "manual") {
                continue;
            }
            
            try {
                const newRate = await RateService.getLiveRate(
                    exchangeRate.baseCurrency,
                    exchangeRate.targetCurrency,
                    exchangeRate.rateSource,
                    exchangeRate.rate
                );
                
                if (newRate > 0 && parseFloat(exchangeRate.rate) !== newRate) {
                    await exchangeRate.update({ rate: newRate });
                    console.log(`✅ [CRON] Updated rate for ${exchangeRate.baseCurrency}/${exchangeRate.targetCurrency} via ${exchangeRate.rateSource} to ${newRate}`);
                }
            } catch (err) {
                console.error(`❌ [CRON] Failed to sync rate for ${exchangeRate.baseCurrency}/${exchangeRate.targetCurrency}:`, err.message);
            }
        }
        
        console.log("✅ [CRON] Exchange Rate Sync Complete.");
    } catch (error) {
        handleCronError("Exchange Rate Sync", error);
    }
};

// ─────────────────────────────────────────────────────────
// SUBSCRIPTION CRON JOBS
// ─────────────────────────────────────────────────────────

/**
 * Calculate end date helper
 */
const calculateEndDate = (startDate, billingInterval) => {
  const end = new Date(startDate);
  if (billingInterval === "yearly") {
    end.setFullYear(end.getFullYear() + 1);
  } else {
    end.setMonth(end.getMonth() + 1);
  }
  return end.toISOString().split("T")[0];
};

/**
 * Process auto-renewals for active subscriptions whose endDate has passed.
 * Runs daily at midnight.
 */
export const processSubscriptionRenewals = async () => {
    try {
        console.log("⏰ [CRON] Running Subscription Renewal Processing...");

        const today = new Date().toISOString().split("T")[0];

        // Find subscriptions that need renewal
        const dueSubscriptions = await MerchantSubscription.findAll({
            where: {
                endDate: { [Op.lte]: today },
                autoRenew: true,
                status: "active",
            },
            include: [
                { model: SubscriptionPlan, as: "plan" },
                { model: Merchant, as: "merchant" },
            ],
        });

        // Also expire cancelled subscriptions past their endDate
        const expiredCancelled = await MerchantSubscription.update(
            { status: "expired" },
            {
                where: {
                    endDate: { [Op.lte]: today },
                    status: "cancelled",
                },
            }
        );

        if (expiredCancelled[0] > 0) {
            console.log(`✅ [CRON] Expired ${expiredCancelled[0]} cancelled subscriptions.`);
        }

        let renewed = 0;
        let failed = 0;

        for (const sub of dueSubscriptions) {
            const t = await sequelize.transaction();
            try {
                const merchant = await Merchant.findByPk(sub.merchantId, { transaction: t, lock: true });
                const price = parseFloat(sub.plan.price);

                if (parseFloat(merchant.balance) >= price) {
                    // Successful renewal
                    merchant.balance = parseFloat(merchant.balance) - price;
                    await merchant.save({ transaction: t });

                    const newStartDate = sub.endDate;
                    const newEndDate = calculateEndDate(newStartDate, sub.plan.billingInterval);

                    sub.startDate = newStartDate;
                    sub.endDate = newEndDate;
                    await sub.save({ transaction: t });

                    await SubscriptionInvoice.create({
                        subscriptionId: sub.id,
                        merchantId: sub.merchantId,
                        planId: sub.planId,
                        amount: price,
                        currency: sub.plan.currency,
                        paymentMethod: "wallet",
                        paymentStatus: "paid",
                        billingPeriodStart: newStartDate,
                        billingPeriodEnd: newEndDate,
                        paidAt: new Date(),
                    }, { transaction: t });

                    await t.commit();
                    renewed++;

                    // Non-blocking renewal email
                    const content = `
                        <p style="font-size: 22px; font-weight: 800; color: #1a1a2e;">Subscription Renewed ✅</p>
                        <p>Hi <strong>${merchant.businessName}</strong>,</p>
                        <p>Your <strong>"${sub.plan.name}"</strong> plan has been automatically renewed.</p>
                        <div style="background: #f8f5ff; border: 1px solid #e9d5ff; border-radius: 10px; padding: 20px; margin: 20px 0;">
                            <p><strong>Amount:</strong> ${sub.plan.currency} ${price.toFixed(2)}</p>
                            <p><strong>Next renewal:</strong> ${newEndDate}</p>
                        </div>
                    `;
                    sendEmail(merchant.email, `Airpero — "${sub.plan.name}" Plan Renewed`, baseEmailLayout({ htmlContent: content, title: "Subscription Renewed" })).catch(() => {});

                } else {
                    // Payment failed
                    sub.status = "expired";
                    sub.autoRenew = false;
                    await sub.save({ transaction: t });

                    await SubscriptionInvoice.create({
                        subscriptionId: sub.id,
                        merchantId: sub.merchantId,
                        planId: sub.planId,
                        amount: price,
                        currency: sub.plan.currency,
                        paymentMethod: "wallet",
                        paymentStatus: "failed",
                        billingPeriodStart: sub.endDate,
                        billingPeriodEnd: calculateEndDate(sub.endDate, sub.plan.billingInterval),
                        paidAt: null,
                    }, { transaction: t });

                    await t.commit();
                    failed++;

                    // Non-blocking failure email
                    const content = `
                        <p style="font-size: 22px; font-weight: 800; color: #dc2626;">Subscription Payment Failed ❌</p>
                        <p>Hi <strong>${merchant.businessName}</strong>,</p>
                        <p>We tried to renew your <strong>"${sub.plan.name}"</strong> plan but your wallet balance is insufficient.</p>
                        <p>Required: <strong>${sub.plan.currency} ${price.toFixed(2)}</strong> | Balance: <strong>${sub.plan.currency} ${parseFloat(merchant.balance).toFixed(2)}</strong></p>
                        <p>Your plan benefits have been removed. Please fund your wallet and resubscribe.</p>
                    `;
                    sendEmail(merchant.email, `Airpero — Subscription Renewal Failed`, baseEmailLayout({ htmlContent: content, title: "Payment Failed" })).catch(() => {});
                }
            } catch (err) {
                await t.rollback();
                console.error(`❌ [CRON] Renewal failed for subscription ${sub.id}:`, err.message);
                failed++;
            }
        }

        if (renewed > 0 || failed > 0) {
            console.log(`✅ [CRON] Subscription Renewals: ${renewed} renewed, ${failed} failed.`);
        }
    } catch (error) {
        console.error("❌ [CRON] Subscription Renewal Error:", error);
    }
};

/**
 * Send renewal reminder emails 3 days and 1 day before expiry.
 * Runs daily at 9 AM.
 */
export const sendRenewalReminders = async () => {
    try {
        console.log("⏰ [CRON] Running Renewal Reminder Check...");

        const today = new Date();
        const threeDaysFromNow = new Date(today);
        threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
        const oneDayFromNow = new Date(today);
        oneDayFromNow.setDate(oneDayFromNow.getDate() + 1);

        const threeDayStr = threeDaysFromNow.toISOString().split("T")[0];
        const oneDayStr = oneDayFromNow.toISOString().split("T")[0];

        // 3-day reminders
        const threeDayReminders = await MerchantSubscription.findAll({
            where: { endDate: threeDayStr, status: "active", autoRenew: true },
            include: [
                { model: SubscriptionPlan, as: "plan" },
                { model: Merchant, as: "merchant" },
            ],
        });

        for (const sub of threeDayReminders) {
            const content = `
                <p style="font-size: 22px; font-weight: 800; color: #1a1a2e;">Subscription Renewal in 3 Days 🔔</p>
                <p>Hi <strong>${sub.merchant.businessName}</strong>,</p>
                <p>Your <strong>"${sub.plan.name}"</strong> subscription will auto-renew on <strong>${sub.endDate}</strong>.</p>
                <p>Amount: <strong>${sub.plan.currency} ${parseFloat(sub.plan.price).toFixed(2)}</strong></p>
                <p>Please ensure your wallet has sufficient funds.</p>
            `;
            sendEmail(sub.merchant.email, `Airpero — Your "${sub.plan.name}" plan renews in 3 days`, baseEmailLayout({ htmlContent: content, title: "Renewal Reminder" })).catch(() => {});
        }

        // 1-day (urgent) reminders
        const oneDayReminders = await MerchantSubscription.findAll({
            where: { endDate: oneDayStr, status: "active", autoRenew: true },
            include: [
                { model: SubscriptionPlan, as: "plan" },
                { model: Merchant, as: "merchant" },
            ],
        });

        for (const sub of oneDayReminders) {
            const content = `
                <p style="font-size: 22px; font-weight: 800; color: #d97706;">⚠️ Subscription Renews Tomorrow!</p>
                <p>Hi <strong>${sub.merchant.businessName}</strong>,</p>
                <p>Your <strong>"${sub.plan.name}"</strong> subscription will auto-renew <strong>tomorrow (${sub.endDate})</strong>.</p>
                <p>Amount: <strong>${sub.plan.currency} ${parseFloat(sub.plan.price).toFixed(2)}</strong></p>
                <div style="background: #fff8ec; border-left: 4px solid #f59e0b; border-radius: 4px; padding: 14px 18px;">
                    <p style="font-size: 13px; color: #78350f; margin: 0;">
                        <strong>Important:</strong> Ensure your wallet is funded to avoid service interruption.
                    </p>
                </div>
            `;
            sendEmail(sub.merchant.email, `Airpero — ⚠️ "${sub.plan.name}" renews TOMORROW`, baseEmailLayout({ htmlContent: content, title: "Urgent Renewal Reminder" })).catch(() => {});
        }

        const total = threeDayReminders.length + oneDayReminders.length;
        if (total > 0) {
            console.log(`✅ [CRON] Sent ${total} renewal reminders (${threeDayReminders.length} × 3-day, ${oneDayReminders.length} × 1-day).`);
        }
    } catch (error) {
        console.error("❌ [CRON] Renewal Reminder Error:", error);
    }
};

/**
 * Resolves payout transactions left "pending" because 9PSB's initial response wasn't a
 * confirmed SUCCESS. Requeries each against 9PSB and applies the real outcome:
 *
 *  - A confirmed SUCCESS resolves immediately (and logs profit) — good news is never delayed.
 *  - Anything else (an explicit FAILED, an unrecognized status, or the requery call itself
 *    erroring out) counts as one "strike" rather than an immediate failure, since 9PSB's own
 *    answer can be transiently wrong before settling on the truth. Only after
 *    MAX_RECONCILIATION_ATTEMPTS strikes with no SUCCESS ever seen is it finally marked
 *    "failed" and the debited balance refunded. Below that threshold it stays "pending" and
 *    is retried on the next run.
 *
 * Only considers payouts between 2 minutes and 48 hours old — too fresh to bother 9PSB with,
 * and anything stuck longer than 48 hours (or that has exhausted its retries) needs manual
 * review instead of continued silent auto-retry.
 */
const MAX_RECONCILIATION_ATTEMPTS = 4;

// Guards against overlapping runs. setInterval fires on a fixed schedule regardless of
// whether the previous invocation has finished — if a single run ever hangs or just runs
// long (a slow/unresponsive provider, network issues), the next scheduled tick would
// otherwise pile another concurrent execution on top of it, all fighting over the same
// stuck transaction(s). This makes each tick a no-op until the current run actually finishes.
let isReconciling = false;

export const reconcilePendingPayouts = async () => {
    if (isReconciling) {
        console.warn("⏰ [CRON] Skipping this run — the previous Pending Payout Reconciliation is still in progress.");
        return;
    }
    isReconciling = true;

    try {
        console.log("⏰ [CRON] Running Pending Payout Reconciliation...");

        const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
        const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000);

        const pendingPayouts = await Transaction.findAll({
            where: {
                status: "pending",
                class: "payout",
                createdAt: { [Op.lt]: twoMinutesAgo, [Op.gt]: fortyEightHoursAgo },
            },
        });

        if (pendingPayouts.length === 0) {
            return;
        }

        console.log(`[CRON] Found ${pendingPayouts.length} pending payout(s) to reconcile.`);

        let resolvedSuccess = 0;
        let resolvedFailed = 0;
        let stillPending = 0;

        for (const tx of pendingPayouts) {
            let psbStatus = null;
            let checkError = null;

            try {
                const result = await PsbService.walletRequery({
                    transactionId: tx.transaction_id,
                    amount: Math.round(parseFloat(tx.amount)),
                    transactionType: "OTHER_BANKS",
                    transactionDate: tx.createdAt.toISOString().split("T")[0],
                    accountNo: "",
                }, "AIRPERO");

                psbStatus = result?.status;
            } catch (err) {
                checkError = err;
                console.error(`❌ [CRON] Reconciliation requery failed for ${tx.transaction_id}:`, err.response?.data || err.message);
            }

            if (psbStatus === "SUCCESS") {
                let alreadyResolved = false;

                await sequelize.transaction(async (t) => {
                    // Re-fetch and lock fresh — if another process (e.g. an overlapping run)
                    // already resolved this transaction between when we loaded it and now,
                    // its status will no longer be "pending". Without this check, a second
                    // process could log a duplicate SystemProfit entry and send a second
                    // webhook for something already handled.
                    const freshTx = await Transaction.findByPk(tx.id, { transaction: t, lock: true });
                    if (!freshTx || freshTx.status !== "pending") {
                        alreadyResolved = true;
                        return;
                    }

                    await freshTx.update(
                        { status: "success", info: `${freshTx.info} [Reconciled: confirmed successful by 9PSB]` },
                        { transaction: t }
                    );
                    await SystemProfit.create({
                        amount: tx.fee,
                        source: "payout_fee",
                        transactionId: tx.transaction_id,
                        merchantId: tx.merchantId,
                    }, { transaction: t });
                });

                if (alreadyResolved) {
                    continue;
                }

                // Tell the merchant it's resolved — this is the only place a pending payout's
                // outcome ever gets pushed out; without it, they'd have no way to know short of
                // polling /transactions/requery themselves.
                WebhookService.notifyMerchant(
                    tx.merchantId,
                    "PAYOUT_SUCCESS",
                    {
                        transaction_id: tx.transaction_id,
                        reference: tx.transaction_id,
                        customerId: tx.customerId,
                        amount: parseFloat(tx.amount),
                        fee: parseFloat(tx.fee),
                        currency: tx.currency,
                        status: "success",
                    },
                    tx.customerId
                ).catch((err) => console.error(`⚠️ [CRON] PAYOUT_SUCCESS webhook failed for ${tx.transaction_id}:`, err.message));

                resolvedSuccess++;
                continue;
            }

            // Not a confirmed success — one strike, whether 9PSB said FAILED, gave an
            // unrecognized status, or the requery itself errored. Only resolve to "failed"
            // (and refund) once the strike count reaches the limit.
            const newAttempts = tx.reconciliationAttempts + 1;
            const reasonNote = checkError ? `requery error: ${checkError.message}` : `provider status: ${psbStatus}`;

            if (newAttempts >= MAX_RECONCILIATION_ATTEMPTS) {
                let alreadyResolved = false;

                await sequelize.transaction(async (t) => {
                    // Re-fetch and lock fresh, same reasoning as the success branch above —
                    // this is the one that moves money, so it matters even more here. Without
                    // this check, another process resolving the same transaction concurrently
                    // could refund the same failed payout more than once.
                    const freshTx = await Transaction.findByPk(tx.id, { transaction: t, lock: true });
                    if (!freshTx || freshTx.status !== "pending") {
                        alreadyResolved = true;
                        return;
                    }

                    if (tx.customerId) {
                        // Customer-initiated payout: customer paid `amount`, merchant paid fee + stamp duty.
                        const customer = await Customer.findByPk(tx.customerId, { transaction: t, lock: true });
                        if (customer) {
                            await customer.increment("balance", { by: parseFloat(tx.amount), transaction: t });
                        }
                        const merchant = await Merchant.findByPk(tx.merchantId, { transaction: t, lock: true });
                        if (merchant) {
                            const merchantRefund = parseFloat(tx.fee) + parseFloat(tx.stampDuty || 0);
                            await merchant.increment("balance", { by: merchantRefund, transaction: t });
                        }
                    } else {
                        // Merchant's own payout: merchant paid amount + fee + stamp duty entirely.
                        const merchant = await Merchant.findByPk(tx.merchantId, { transaction: t, lock: true });
                        if (merchant) {
                            const totalRefund = parseFloat(tx.amount) + parseFloat(tx.fee) + parseFloat(tx.stampDuty || 0);
                            await merchant.increment("balance", { by: totalRefund, transaction: t });
                        }
                    }

                    await freshTx.update(
                        {
                            status: "failed",
                            reconciliationAttempts: newAttempts,
                            info: `${freshTx.info} [Reconciled: confirmed failed by 9PSB after ${newAttempts} checks, funds refunded]`,
                        },
                        { transaction: t }
                    );
                });

                if (alreadyResolved) {
                    continue;
                }

                // Tell the merchant it's resolved — same reasoning as the success case above.
                WebhookService.notifyMerchant(
                    tx.merchantId,
                    "PAYOUT_FAILED",
                    {
                        transaction_id: tx.transaction_id,
                        reference: tx.transaction_id,
                        customerId: tx.customerId,
                        amount: parseFloat(tx.amount),
                        fee: parseFloat(tx.fee),
                        currency: tx.currency,
                        status: "failed",
                        reason: "Confirmed failed by the provider after repeated verification.",
                        refunded: true,
                    },
                    tx.customerId
                ).catch((err) => console.error(`⚠️ [CRON] PAYOUT_FAILED webhook failed for ${tx.transaction_id}:`, err.message));

                resolvedFailed++;
            } else {
                await tx.update({
                    reconciliationAttempts: newAttempts,
                    info: `${tx.info} [Check ${newAttempts}/${MAX_RECONCILIATION_ATTEMPTS}: ${reasonNote} — not yet confirmed, will retry]`,
                });
                stillPending++;
            }
        }

        console.log(`✅ [CRON] Payout Reconciliation Complete. Resolved ${resolvedSuccess} as success, ${resolvedFailed} as failed, ${stillPending} still awaiting more checks.`);
    } catch (error) {
        console.error("❌ [CRON] Payout Reconciliation Error:", error);
    } finally {
        isReconciling = false;
    }
};
