const { Transaction, User, UserBalance, sequelize } = require("../../models");
const { Op } = require("sequelize");
const { ValidationError, NotFoundError } = require("../../utils/errors");
const { generateTransactionId, notifyUser, sendEmail } = require("../../utils/helpers");
const userService = require("./userService");

// Allowed enums (must match your model)
const SOURCE = ["funding", "wallet"];
const TO_TYPE = ["internal", "external"];
const DIRECTION = ["credit", "debit"];
const STATUS = ["initiated", "pending", "processing", "completed", "failed", "cancelled"];

const KIND = [
  "deposit", "withdrawal", "transfer_in", "transfer_out", "bonus", "swap", "staking",
  "signal_buy", "bot_buy", "trade_profit",
  "trade_buy", "trade_sell", "spot", "buy_stock", "sell_stock", "fee", "referral_bonus",
  "staking_reward", "savings_interest", "launchpad_purchase", "investment", "roi",
  "airdrop", "adjustment",
];

const toNum = (v) => {
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
};

const pick = (obj, keys) => {
  const out = {};
  keys.forEach((k) => {
    if (obj[k] !== undefined) out[k] = obj[k];
  });
  return out;
};

// IMPORTANT NOTE ABOUT AMOUNT USED:
// - I credit/refund tx.netAmount if present, otherwise tx.amount.
// - If your business logic wants "amount" (not net) credited/refunded, change computeDelta() below.
const computeDelta = (tx) => {
  const net = toNum(tx.netAmount);
  const amt = toNum(tx.amount);
  const delta = net !== null ? net : (amt !== null ? amt : 0);
  return delta;
};

async function getOrCreateUserBalance(userId, t) {
  let bal = await UserBalance.findOne({
    where: { userId },
    transaction: t,
    lock: t.LOCK.UPDATE,
  });

  if (!bal) {
    // If your schema requires other fields, adjust here
    bal = await UserBalance.create(
      { userId, balance: 0, bonus: 0 },
      { transaction: t }
    );
  }

  return bal;
}

async function addToUserBalance(userId, amount, t) {
  const bal = await getOrCreateUserBalance(userId, t);
  const current = Number(bal.balance || 0);
  const next = current + Number(amount || 0);
  await bal.update({ balance: next }, { transaction: t });
  return bal;
}

class adminTransactionService {
  async getTransactions(query = {}, urlBase = "") {
    const {
      q,
      userId,
      transactionId,
      kind,
      status,
      direction,
      source,
      toType,
      currency,
      dateFrom,
      dateTo,
      page = 1,
      limit = 20,
    } = query;

    const offset = (Number(page) - 1) * Number(limit);
    const where = {};

    if (userId) where.userId = userId;
    if (transactionId) where.transactionId = transactionId;

    if (kind) where.kind = kind;
    if (status) where.status = status;
    if (direction) where.direction = direction;
    if (source) where.source = source;
    if (toType) where.toType = toType;
    if (currency) where.currency = currency;

    if (dateFrom || dateTo) {
      where.created_at = {};
      if (dateFrom) where.created_at[Op.gte] = new Date(dateFrom);
      if (dateTo) where.created_at[Op.lte] = new Date(dateTo);
    }

    if (q) {
      where[Op.or] = [
        { transactionId: { [Op.like]: `%${q}%` } },
        { txHash: { [Op.like]: `%${q}%` } },
        { address: { [Op.like]: `%${q}%` } },
        { tradePair: { [Op.like]: `%${q}%` } },
      ];
    }


    const { rows, count } = await Transaction.findAndCountAll({
      where,
      order: [["created_at", "DESC"]],
      limit: Number(limit),
      offset,
      include: [{ model: User, as: "user", attributes: ["id", "email", "firstName", "lastName"] }],
    });

    return {
      count,
      page: Number(page),
      limit: Number(limit),
      transactions: rows.map(r => {
        const baseUrl = urlBase || "";

        const txData = {
          id: r.id,
          source: r.source,
          transactionId: r.transactionId,
          userEmail: r.user?.email,
          userId: r.user?.id,
          toType: r.toType,
          kind: r.kind,
          currency: r.currency,
          amount: r.amount,
          fee: r.fee,
          netAmount: r.netAmount,
          status: r.status,
          created_at: r.createdAt,
        }
        if (r.kind === "deposit") {
          txData.explorerLink = `${baseUrl}/explorer/${r.network}/${r.txHash}`;
          txData.depositProof = `${baseUrl}/uploads/deposits/${r.metadata?.proof}`;
        }
        return txData;
      }),
    };
  }

  async getDeposits(query = {}, urlBase) {
    return this.getTransactions({ ...query, kind: "deposit" }, urlBase);
  }

  async getWithdrawals(query = {}) {
    return this.getTransactions({ ...query, kind: "withdrawal" });
  }

  async getTransactionById(txId) {
    const where = { deleted_at: { [Op.is]: null } };

    const tx = await Transaction.findOne({
      where: {
        ...where,
        [Op.or]: [{ id: txId }, { transactionId: txId }],
      },
      include: [{ model: User, as: "user", attributes: ["id", "email", "firstName", "lastName"] }],
    });

    if (!tx) throw new NotFoundError("Transaction not found");
    return { transaction: tx };
  }

  validatePayload(data, { partial = false } = {}) {
    const must = (k) => !partial && (data[k] === undefined || data[k] === null || data[k] === "");

    if (must("userId")) throw new ValidationError("userId is required");
    if (must("source")) throw new ValidationError("source is required");
    if (must("toType")) throw new ValidationError("toType is required");
    if (must("kind")) throw new ValidationError("kind is required");
    if (must("direction")) throw new ValidationError("direction is required");
    if (must("currency")) throw new ValidationError("currency is required");
    if (must("amount")) throw new ValidationError("amount is required");

    if (data.source !== undefined && !SOURCE.includes(data.source)) {
      throw new ValidationError(`source must be one of: ${SOURCE.join(", ")}`);
    }
    if (data.toType !== undefined && !TO_TYPE.includes(data.toType)) {
      throw new ValidationError(`toType must be one of: ${TO_TYPE.join(", ")}`);
    }
    if (data.kind !== undefined && !KIND.includes(data.kind)) {
      throw new ValidationError(`kind must be one of: ${KIND.join(", ")}`);
    }
    if (data.direction !== undefined && !DIRECTION.includes(data.direction)) {
      throw new ValidationError(`direction must be one of: ${DIRECTION.join(", ")}`);
    }
    if (data.status !== undefined && !STATUS.includes(data.status)) {
      throw new ValidationError(`status must be one of: ${STATUS.join(", ")}`);
    }

    if (data.amount !== undefined) {
      const a = toNum(data.amount);
      if (a === null || a < 0) throw new ValidationError("amount must be >= 0");
    }
    if (data.fee !== undefined) {
      const f = toNum(data.fee);
      if (f === null || f < 0) throw new ValidationError("fee must be >= 0");
    }
  }

  async createTransaction(data, adminId) {
    this.validatePayload(data, { partial: false });
    const user = await User.findByPk(data.userId);
    if (!user) throw new NotFoundError("User not found");

    const payload = {
      transactionId: data.transactionId || generateTransactionId(),
      userId: data.userId,
      source: data.source,
      toType: data.toType,
      kind: data.kind,
      direction: data.direction,
      currency: data.currency,
      amount: data.amount ?? 0,
      fee: data.fee ?? 0,
      feeCurrency: data.feeCurrency ?? null,
      netAmount: data.netAmount ?? null,

      planId: data.planId ?? null,
      methodId: data.methodId ?? null,
      walletId: data.walletId ?? null,
      fromWalletId: data.fromWalletId ?? null,
      toWalletId: data.toWalletId ?? null,

      network: data.network ?? null,
      address: data.address ?? null,
      txHash: data.txHash ?? null,
      confirmations: data.confirmations ?? 0,
      requiredConfirmations: data.requiredConfirmations ?? 0,

      orderId: data.orderId ?? null,
      tradePair: data.tradePair ?? null,
      price: data.price ?? null,
      quantity: data.quantity ?? null,

      balanceBefore: data.balanceBefore ?? null,
      balanceAfter: data.balanceAfter ?? null,

      status: data.status || "pending",
      processedAt: data.processedAt ?? null,
      completedAt: data.completedAt ?? null,
      failedAt: data.failedAt ?? null,
      cancelledAt: data.cancelledAt ?? null,

      errorMessage: data.errorMessage ?? null,
      errorCode: data.errorCode ?? null,

      description: data.description ?? null,
      notes: data.notes ?? null,

      ip: data.ip ?? null,
      device: data.device ?? null,

      metadata: data.metadata ?? {},
      createdByAdminId: adminId ?? null, // safe if column not present
    };

    const tx = await Transaction.create(payload);
    return { transaction: tx };
  }

  async updateTransaction(txId, data, adminId) {
    this.validatePayload(data, { partial: true });

    return await sequelize.transaction(async (t) => {
      const tx = await Transaction.findOne({
        where: { [Op.or]: [{ id: txId }, { transactionId: txId }] },
        transaction: t,
        lock: t.LOCK.UPDATE,
      });
      if (!tx) throw new NotFoundError("Transaction not found");

      const user = await User.findOne({ where: { id: tx.userId }, transaction: t });
      if (!user) throw new NotFoundError("User not found");

      const prevStatus = tx.status;

      const updatable = pick(data, [
        "amount", "status", "processedAt", "completedAt",
        "failedAt", "cancelledAt", "updatedByAdminId"
      ]);

      if (updatable.metadata && typeof updatable.metadata === "object") {
        updatable.metadata = { ...(tx.metadata || {}), ...updatable.metadata };
      }

      if (updatable.status) {
        const now = new Date();
        if (updatable.status === "processing" && !updatable.processedAt) updatable.processedAt = now;
        if (updatable.status === "completed" && !updatable.completedAt) updatable.completedAt = now;
        if (updatable.status === "failed" && !updatable.failedAt) updatable.failedAt = now;
        if (updatable.status === "cancelled" && !updatable.cancelledAt) updatable.cancelledAt = now;
      }

      updatable.updatedByAdminId = adminId ?? null;
      await tx.update(updatable, { transaction: t });

      const nextStatus = tx.status;
      const meta = tx.metadata || {};

      const depositMessages = {
        processing: {
          title: "Deposit Being Processed",
          message: `We've received your deposit request for ${tx.transactionId} and it's currently being processed. This usually takes a few minutes.`,
        },
        completed: {
          title: "Deposit Successful 🎉",
          message: `Your deposit of $${tx.amount} has been confirmed and added to your balance. You're all set to start trading!`,
        },
        failed: {
          title: "Deposit Failed",
          message: `Unfortunately, your deposit of $${tx.amount} could not be completed. Please check your payment details and try again, or contact support if the issue persists.`,
        },
        cancelled: {
          title: "Deposit Cancelled",
          message: `Your deposit request of $${tx.amount} has been cancelled. If you didn't request this, please contact our support team immediately.`,
        },
      };

      const withdrawalMessages = {
        processing: {
          title: "Withdrawal Being Processed",
          message: `Your withdrawal request of $${tx.amount} is on its way. We're processing it now and you'll be notified once it's sent.`,
        },
        completed: {
          title: "Withdrawal Successful",
          message: `Your withdrawal of $${tx.amount} has been sent successfully. Depending on your method, it may take 1–3 business days to reflect in your account.`,
        },
        failed: {
          title: "Withdrawal Failed — Funds Returned",
          message: `We were unable to process your withdrawal of $${tx.amount}. Don't worry — your funds have been returned to your balance. Please try again or reach out to support for help.`,
        },
        cancelled: {
          title: "Withdrawal Cancelled",
          message: `Your withdrawal request of $${tx.amount} has been cancelled and your funds have been returned to your balance. Contact support if you have any questions.`,
        },
      };

      // ─────────────────────────────────────────
      // DEPOSIT
      // ─────────────────────────────────────────
      if (tx.kind === "deposit") {
        const becameCompleted = prevStatus !== "completed" && nextStatus === "completed";
        const becameFailed = prevStatus !== "failed" && nextStatus === "failed";
        const becameCancelled = prevStatus !== "cancelled" && nextStatus === "cancelled";

        // ✅ Completed — credit balance (once)
        if (becameCompleted && !meta.balanceApplied) {
          const delta = computeDelta(tx);
          if (delta > 0) {
            await addToUserBalance(tx.userId, delta, t);

            await tx.update(
              { metadata: { ...(tx.metadata || {}), balanceApplied: true, balanceAppliedAt: new Date().toISOString() } },
              { transaction: t }
            );

            await userService.notifyUser(tx.userId, {
              ...depositMessages.completed,
              type: "deposit",
              channels: { inApp: true, push: true },
              priority: "medium",
            }, adminId);

            sendEmail({
              type: "deposit_approved",
              to: user.email,
              data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
              subject: "Deposit Approved",
            });
          }
        }

        // 🔴 Failed
        if (becameFailed) {
          await userService.notifyUser(tx.userId, {
            ...depositMessages.failed,
            type: "deposit",
            channels: { inApp: true, push: true },
            priority: "high",
          }, adminId);

          sendEmail({
            type: "deposit_failed",
            to: user.email,
            data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
            subject: "Deposit Failed",
          });
        }

        // 🚫 Cancelled
        if (becameCancelled) {
          await userService.notifyUser(tx.userId, {
            ...depositMessages.cancelled,
            type: "deposit",
            channels: { inApp: true, push: true },
            priority: "medium",
          }, adminId);

          sendEmail({
            type: "deposit_cancelled",
            to: user.email,
            data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
            subject: "Deposit Cancelled",
          });
        }
      }

      // ─────────────────────────────────────────
      // WITHDRAWAL
      // ─────────────────────────────────────────
      if (tx.kind === "withdrawal") {
        const becameCompleted = prevStatus !== "completed" && nextStatus === "completed";
        const becameFailed = prevStatus !== "failed" && nextStatus === "failed";
        const becameCancelled = prevStatus !== "cancelled" && nextStatus === "cancelled";

        // ✅ Completed — notify only (balance already deducted at request time)
        if (becameCompleted) {
          await userService.notifyUser(tx.userId, {
            ...withdrawalMessages.completed,
            type: "withdrawal",
            channels: { inApp: true, push: true },
            priority: "medium",
          }, adminId);

          sendEmail({
            type: "withdrawal_approved",
            to: user.email,
            data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
            subject: "Withdrawal Approved",
          });
        }

        // 🔴 Failed — refund balance (once)
        if (becameFailed && !meta.refundApplied) {
          const delta = computeDelta(tx);
          if (delta > 0) {
            await addToUserBalance(tx.userId, delta, t);

            await tx.update(
              { metadata: { ...(tx.metadata || {}), refundApplied: true, refundAppliedAt: new Date().toISOString() } },
              { transaction: t }
            );

            await userService.notifyUser(tx.userId, {
              ...withdrawalMessages.failed,
              type: "withdrawal",
              channels: { inApp: true, push: true },
              priority: "high",
            }, adminId);

            sendEmail({
              type: "withdrawal_failed",
              to: user.email,
              data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
              subject: "Withdrawal Failed — Funds Returned",
            });
          }
        }

        // 🚫 Cancelled — refund balance (once)
        if (becameCancelled && !meta.cancelRefundApplied) {
          const delta = computeDelta(tx);
          if (delta > 0) {
            await addToUserBalance(tx.userId, delta, t);

            await tx.update(
              { metadata: { ...(tx.metadata || {}), cancelRefundApplied: true, cancelRefundAppliedAt: new Date().toISOString() } },
              { transaction: t }
            );

            await userService.notifyUser(tx.userId, {
              ...withdrawalMessages.cancelled,
              type: "withdrawal",
              channels: { inApp: true, push: true },
              priority: "medium",
            }, adminId);

            sendEmail({
              type: "withdrawal_cancelled",
              to: user.email,
              data: { currency: user.currency, amount: tx.amount, reference: tx.transactionId },
              subject: "Withdrawal Cancelled — Funds Returned",
            });
          }
        }
      }

      return { transaction: tx };
    });
  }

  async deleteTransaction(txId, adminId) {
    const tx = await Transaction.findOne({
      where: {
        [Op.or]: [{ id: txId }, { transactionId: txId }],
      },
    });
    if (!tx) throw new NotFoundError("Transaction not found");
    await tx.destroy();
    /*  await tx.update({
       deleted_at: new Date(),
       deletedByAdminId: adminId ?? null,
     }); */

    return { deleted: true };
  }
}

module.exports = new adminTransactionService();