"use strict";

const { Op } = require("sequelize");
const {
  Wallet,
  WalletAddress,
  Transaction,
  User,
  Coin,
  Network,
  NetworkAddress,
  sequelize,
  Asset,
  UserBalance,
  DerivativeAccount,
  MarketPair,
} = require("../../models");

const { ValidationError, NotFoundError, ConflictError, BadRequestError } = require("../../utils/errors");
const { sendEmail } = require("../../utils/helpers");
const userService = require("./userService");

function toNum(x) {
  const n = Number(x);
  return Number.isFinite(n) ? n : NaN;
}

class adminWalletService {
  // -----------------------------
  // Wallets CRUD
  // -----------------------------

  async listCoins(query = {}) {
    const coins = Asset.findAll({
      where: {
        type: 'crypto',
        allowWallet: true
      },
    })
    return coins
  }
  async listWallets(query = {}) {
    const page = Math.max(parseInt(query.page || "1", 10), 1);
    const limit = Math.min(Math.max(parseInt(query.limit || "20", 10), 1), 100);
    const offset = (page - 1) * limit;

    const where = {};
    if (query.userId) where.userId = query.userId;
    if (query.coinId) where.coinId = query.coinId;
    if (typeof query.status !== "undefined" && query.status !== "") where.status = String(query.status) === "true";
    if (typeof query.lockStatus !== "undefined" && query.lockStatus !== "") where.lockStatus = String(query.lockStatus) === "true";

    if (query.q) {
      // basic search by reference only (safe)
      where[Op.or] = [{ reference: { [Op.like]: `%${query.q}%` } }];
    }

    const { rows, count } = await Wallet.findAndCountAll({
      where,
      order: [["created_at", "DESC"]],
      limit,
      offset,
      include: [
        { model: User, as: "user", required: true },
        { model: Asset, as: "coin", required: true },
      ],
    });

    return { count, page, limit, wallets: rows };
  }

  async getWalletById(id) {
    const wallet = await Wallet.findByPk(id, {
      include: [
        { model: User, as: "user", required: false },
        { model: Asset, as: "coin", required: false },
      ],
    });
    if (!wallet) throw new NotFoundError("Wallet not found");
    return { wallet };
  }

  async createWallet(data) {
    const userId = toNum(data?.userId);
    const coinId = toNum(data?.coinId);
    const reference = String(data?.reference || "").trim();

    if (!reference) throw new ValidationError("reference is required");
    if (!Number.isFinite(userId) || userId <= 0) throw new ValidationError("userId is required");
    if (!Number.isFinite(coinId) || coinId <= 0) throw new ValidationError("coinId is required");

    const user = await User.findByPk(userId);
    if (!user) throw new NotFoundError("User not found");

    const coin = await Asset.findByPk(coinId);
    if (!coin) throw new NotFoundError("Coin not found");

    const existsRef = await Wallet.findOne({ where: { reference } });
    if (existsRef) throw new ConflictError("Wallet reference already exists");

    const existsPair = await Wallet.findOne({ where: { userId, coinId } });
    if (existsPair) throw new ConflictError("Wallet already exists for this user + coin");

    const wallet = await Wallet.create({
      reference,
      userId,
      coinId,
      balance: data?.balance ?? 0,
      lockedBalance: data?.lockedBalance ?? 0,
      lockStatus: Boolean(data?.lockStatus ?? false),
      lockReason: data?.lockReason ?? null,
      lockedAt: data?.lockStatus ? new Date() : null,
      allowDeposit: typeof data?.allowDeposit === "boolean" ? data.allowDeposit : true,
      allowSend: typeof data?.allowSend === "boolean" ? data.allowSend : true,
      allowSwap: typeof data?.allowSwap === "boolean" ? data.allowSwap : true,
      status: typeof data?.status === "boolean" ? data.status : true,
    });

    return wallet;
  }

  async updateWallet(id, data) {
    const wallet = await Wallet.findByPk(id);
    if (!wallet) throw new NotFoundError("Wallet not found");

    if (data?.reference && String(data.reference).trim() !== String(wallet.reference)) {
      const exists = await Wallet.findOne({
        where: { reference: String(data.reference).trim(), id: { [Op.ne]: id } },
      });
      if (exists) throw new ConflictError("Wallet reference already exists");
    }

    const lockStatus = typeof data?.lockStatus === "boolean" ? data.lockStatus : wallet.lockStatus;

    await wallet.update({
      reference: typeof data?.reference !== "undefined" ? String(data.reference).trim() : wallet.reference,

      // DON'T allow changing userId/coinId by default (it causes integrity issues)
      // If you want it, tell me and I’ll add safe migration logic.

      lockStatus,
      lockReason: typeof data?.lockReason !== "undefined" ? data.lockReason : wallet.lockReason,
      lockedAt: lockStatus ? (wallet.lockedAt ?? new Date()) : null,

      allowDeposit: typeof data?.allowDeposit === "boolean" ? data.allowDeposit : wallet.allowDeposit,
      allowSend: typeof data?.allowSend === "boolean" ? data.allowSend : wallet.allowSend,
      allowSwap: typeof data?.allowSwap === "boolean" ? data.allowSwap : wallet.allowSwap,

      status: typeof data?.status === "boolean" ? data.status : wallet.status,
    });

    return wallet;
  }

  async deleteWallet(id) {
    const wallet = await Wallet.findByPk(id);
    if (!wallet) throw new NotFoundError("Wallet not found");

    const addrCount = await WalletAddress.count({ where: { userId: wallet.userId, coinId: wallet.coinId } });
    if (addrCount > 0) throw new ConflictError("Cannot delete wallet: wallet has addresses");

    await wallet.destroy(); // paranoid -> soft delete
    return true;
  }

  // -----------------------------
  // WalletAddress CRUD
  // -----------------------------
  async listWalletAddresses(query = {}) {
    const page = Math.max(parseInt(query.page || "1", 10), 1);
    const limit = Math.min(Math.max(parseInt(query.limit || "20", 10), 1), 100);
    const offset = (page - 1) * limit;
    const where = {};
    if (query.userId) where.userId = query.userId;
    if (query.coinId) where.coinId = query.coinId;
    if (query.networkId) where.networkId = query.networkId;
    if (query.networkAddressId) where.networkAddressId = query.networkAddressId;
    if (query.status) where.status = query.status;

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

    const { rows, count } = await WalletAddress.findAndCountAll({
      where,
      order: [["created_at", "DESC"]],
      limit,
      offset,
      include: [
        { model: User, as: "user", required: false },
        { model: Asset, as: "coin", required: false },
        { model: Network, as: "network", required: false },
      ],
    });

    return { count, page, limit, walletAddresses: rows };
  }

  async getWalletAddressById(id) {
    const address = await WalletAddress.findByPk(id, {
      include: [
        { model: User, as: "user", required: false },
        { model: Asset, as: "coin", required: false },
        { model: Network, as: "network", required: false },
      ],
    });
    if (!address) throw new NotFoundError("Wallet address not found");
    return { address };
  }

  async createWalletAddress(data) {
    const userId = toNum(data?.userId);
    const coinId = toNum(data?.coinId);
    const networkId = toNum(data?.networkId);
    const networkAddressId = toNum(data?.networkAddressId);
    const address = String(data?.address || "").trim();
    const status = data?.status || "active";

    if (!Number.isFinite(userId) || userId <= 0) throw new ValidationError("userId is required");
    if (!Number.isFinite(coinId) || coinId <= 0) throw new ValidationError("coinId is required");
    if (!Number.isFinite(networkId) || networkId <= 0) throw new ValidationError("networkId is required");
    if (!Number.isFinite(networkAddressId) || networkAddressId <= 0) throw new ValidationError("networkAddressId is required");
    if (!address) throw new ValidationError("address is required");
    if (!["used", "active", "archived"].includes(status)) throw new ValidationError("Invalid status");

    const [user, coin, network, netAddr] = await Promise.all([
      User.findByPk(userId),
      Asset.findByPk(coinId),
      Network.findByPk(networkId),
      NetworkAddress.findByPk(networkAddressId),
    ]);

    if (!user) throw new NotFoundError("User not found");
    if (!coin) throw new NotFoundError("Coin not found");
    if (!network) throw new NotFoundError("Network not found");
    if (!netAddr) throw new NotFoundError("NetworkAddress not found");

    // avoid duplicates
    const exists = await WalletAddress.findOne({ where: { userId, coinId, networkId, address } });
    if (exists) throw new ConflictError("Wallet address already exists");

    return await WalletAddress.create({
      userId,
      coinId,
      networkId,
      networkAddressId,
      address,
      status,
    });
  }

  async updateWalletAddress(id, data) {
    const row = await WalletAddress.findByPk(id);
    if (!row) throw new NotFoundError("Wallet address not found");

    if (data?.status && !["used", "active", "archived"].includes(data.status)) {
      throw new ValidationError("Invalid status");
    }

    // optional uniqueness check if address changes
    if (typeof data?.address !== "undefined" && String(data.address).trim() !== String(row.address)) {
      const exists = await WalletAddress.findOne({
        where: {
          userId: data?.userId ?? row.userId,
          coinId: data?.coinId ?? row.coinId,
          networkId: data?.networkId ?? row.networkId,
          address: String(data.address).trim(),
          id: { [Op.ne]: id },
        },
      });
      if (exists) throw new ConflictError("Wallet address already exists");
    }

    await row.update({
      // keep ids editable only if you really want. It’s okay here, but be careful.
      userId: typeof data?.userId !== "undefined" ? data.userId : row.userId,
      coinId: typeof data?.coinId !== "undefined" ? data.coinId : row.coinId,
      networkId: typeof data?.networkId !== "undefined" ? data.networkId : row.networkId,
      networkAddressId: typeof data?.networkAddressId !== "undefined" ? data.networkAddressId : row.networkAddressId,
      address: typeof data?.address !== "undefined" ? String(data.address).trim() : row.address,
      status: typeof data?.status !== "undefined" ? data.status : row.status,
    });

    return row;
  }

  async deleteWalletAddress(id) {
    const row = await WalletAddress.findByPk(id);
    if (!row) throw new NotFoundError("Wallet address not found");
    await row.destroy();
    return true;
  }

  // -----------------------------
  // Actions (Deposit / Withdraw / Swap) => create Transaction
  // -----------------------------

  async depositToWallet(walletId, body = {}, adminId) {
    const amount = toNum(body?.amount);
    if (!Number.isFinite(amount) || amount <= 0) throw new ValidationError("amount must be > 0");

    const wallet = await Wallet.findByPk(walletId, { include: [{ model: Asset, as: "coin", required: false }] });
    if (!wallet) throw new NotFoundError("Wallet not found");
    if (!wallet.status) throw new BadRequestError("Wallet is disabled");
    if (wallet.lockStatus) throw new BadRequestError("Wallet is locked");
    if (!wallet.allowDeposit) throw new BadRequestError("Deposit not allowed for this wallet");

    const user = await User.findOne({
      id: wallet.userId
    })
    if (!user) throw new NotFoundError("User not found");
    const currency = body?.currency || wallet?.coin?.symbol || "N/A";
    const coinName = wallet?.coin?.name || "N/A"
    const fee = toNum(body?.fee || 0) || 0;

    return await sequelize.transaction(async (t) => {
      const before = Number(wallet.balance);
      const after = before + amount;

      await wallet.update({ balance: after }, { transaction: t });


      const trx = await Transaction.create(
        {
          userId: wallet.userId,
          source: "wallet",
          toType: "internal",
          kind: "deposit",
          direction: "credit",
          currency,
          amount,
          fee,
          feeCurrency: body?.feeCurrency || currency,
          walletId: wallet.id,
          network: body?.network || null,
          address: body?.address || null,
          txHash: body?.txHash || null,
          confirmations: body?.confirmations ?? 0,
          requiredConfirmations: body?.requiredConfirmations ?? 0,
          balanceBefore: before,
          balanceAfter: after,
          status: body?.status || "completed",
          description: body?.description || "Admin deposit",
          notes: body?.notes || null,
          ip: body?.ip || null,
          device: body?.device || null,
          metadata: {
            ...(body?.metadata || {}),
            adminId: adminId || null,
          },
        },
        { transaction: t }
      );

      await userService.notifyUser(wallet.userId, {
        title: `${amount} ${currency} deposited `,
        message: `Deposit of ${amount} ${currency} has been confirmed and added to your wallet balance. `,
        type: 'deposit',
        channels: { inApp: true, push: true },
        priority: "medium",
      }, adminId);

      sendEmail({
        type: "wallet_deposit",
        to: user.email,
        data: { 
          currency, 
          amount, 
          walletName: coinName,
          reference: trx.transactionId,
          date: new Date()
        },
        subject: "Withdrawal Cancelled — Funds Returned",
      });
      return { wallet, transaction: trx };
    });
  }

  async withdrawFromWallet(walletId, body = {}, adminId) {
    const amount = toNum(body?.amount);
    const rate = toNum(body?.rate);
    const amountConverted = toNum(amount * rate)
    if (!Number.isFinite(amount) || amount <= 0) throw new ValidationError("amount must be > 0");

    const wallet = await Wallet.findByPk(walletId, { include: [{ model: Asset, as: "coin", required: true }, { model: User, as: "user", required: true }] });
    if (!wallet) throw new NotFoundError("Wallet not found");
    if (!wallet.status) throw new BadRequestError("Wallet is disabled");
    if (wallet.lockStatus) throw new BadRequestError("Wallet is locked");

    // allowSend maps to "withdraw/send out"
    if (!wallet.allowSend) throw new BadRequestError("Withdrawal/send not allowed for this wallet");

    const currency = body?.currency || wallet?.coin?.symbol || "N/A";
    const fee = toNum(body?.fee || 0) || 0;
    const total = amount + fee;


    return await sequelize.transaction(async (t) => {
      // reload FOR UPDATE style safety (sequelize doesn't do FOR UPDATE by default on all dbs; still ok with tx)
      const fresh = await Wallet.findByPk(wallet.id, { transaction: t });
      const before = Number(fresh.balance);

      if (body?.toType === "funding") {
        const balance = UserBalance.findOne({
          where: { userId: wallet?.userId }
        },
          {
            transaction: t,
            lock: t.LOCK.UPDATE,
          }
        )
        if (!balance) throw new ConflictError("User must have a wallet/balance");
        const balanceUpdate = balance.balance + amountConverted;
      }

      if (body?.toType === "derivative") {
        const balance = DerivativeAccount.findOne({
          where: { userId: wallet?.userId }
        },
          {
            transaction: t,
            lock: t.LOCK.UPDATE,
          }
        )
        if (!balance) throw new ConflictError("User must have a derivative/balance");
        const balanceUpdate = balance.balance + amountConverted;
      }

      if (before < total) throw new BadRequestError("Insufficient balance");

      const after = before - total;
      await fresh.update({ balance: after }, { transaction: t });

      const trx = await Transaction.create(
        {
          userId: fresh.userId,
          source: "wallet",
          toType: body?.toType || "external",
          kind: "withdrawal",
          direction: "debit",
          currency,
          amount,
          fee,
          feeCurrency: body?.feeCurrency || currency,
          walletId: fresh.id,
          network: body?.network || null,
          address: body?.address || null,
          txHash: body?.txHash || null,
          confirmations: body?.confirmations ?? 0,
          requiredConfirmations: body?.requiredConfirmations ?? 0,
          balanceBefore: before,
          balanceAfter: after,
          status: body?.status || "completed",
          description: body?.description || "Admin withdrawal",
          notes: body?.notes || null,
          ip: body?.ip || null,
          device: body?.device || null,
          metadata: {
            ...(body?.metadata || {}),
            adminId: adminId || null,
          },
        },
        { transaction: t }
      );

      if (body?.toType === "funding") {

        await Transaction.create({
          userId,
          source: 'funding',
          toType: 'internal',
          kind: 'deposit',
          direction: 'credit',
          currency: wallet.user?.currency ?? 'USD',
          amount: amountConverted,
          netAmount: amountConverted,
          description: `Funding to balance via ${wallet.coin?.symbol} wallet`,
          status: 'completed'
        }, { transaction: t })

        await balance.update(
          { balance: balanceUpdate }, // debit original amount (or finalAmount if you only want to debit discounted)
          { transaction: t }
        );
      }

      if (body?.toType === "derivative") {

        await Transaction.create({
          userId,
          source: 'funding',
          toType: 'internal',
          kind: 'deposit',
          direction: 'credit',
          currency: wallet.user?.currency ?? 'USD',
          amount: amountConverted,
          netAmount: amountConverted,
          description: `Funding to derivative account via ${wallet.coin?.symbol} wallet`,
          status: 'completed'
        }, { transaction: t })

        await balance.update(
          { balance: balanceUpdate }, // debit original amount (or finalAmount if you only want to debit discounted)
          { transaction: t }
        );
      }

      return { wallet: fresh, transaction: trx };
    });
  }

  async swapWallet(fromWalletId, body = {}, adminId) {
    const toWalletId = toNum(body?.toWalletId);
    const amountFrom = toNum(body?.amount); // amount taken from fromWallet
    const rate = toNum(body?.rate); // toAmount = amountFrom * rate
    const fee = toNum(body?.fee || 0) || 0; // fee in from currency unless you define otherwise

    if (!Number.isFinite(toWalletId) || toWalletId <= 0) throw new ValidationError("toWalletId is required");
    if (!Number.isFinite(amountFrom) || amountFrom <= 0) throw new ValidationError("amount must be > 0");
    if (!Number.isFinite(rate) || rate <= 0) throw new ValidationError("rate must be > 0");

    const fromWallet = await Wallet.findByPk(fromWalletId, { include: [{ model: Asset, as: "coin", required: false }] });
    if (!fromWallet) throw new NotFoundError("From wallet not found");
    if (!fromWallet.status) throw new BadRequestError("From wallet is disabled");
    if (fromWallet.lockStatus) throw new BadRequestError("From wallet is locked");
    if (!fromWallet.allowSwap) throw new BadRequestError("Swap not allowed for from wallet");

    const toWallet = await Wallet.findByPk(toWalletId, { include: [{ model: Asset, as: "coin", required: false }] });
    if (!toWallet) throw new NotFoundError("To wallet not found");
    if (Number(toWallet.userId) !== Number(fromWallet.userId)) throw new BadRequestError("Swap must be within same user");
    if (!toWallet.status) throw new BadRequestError("To wallet is disabled");
    if (toWallet.lockStatus) throw new BadRequestError("To wallet is locked");

    const fromCurrency = body?.fromCurrency || fromWallet?.coin?.symbol || "FROM";
    const toCurrency = body?.toCurrency || toWallet?.coin?.symbol || "TO";

    const toAmount = amountFrom * rate;

    return await sequelize.transaction(async (t) => {
      const freshFrom = await Wallet.findByPk(fromWallet.id, { transaction: t });
      const freshTo = await Wallet.findByPk(toWallet.id, { transaction: t });

      const fromBefore = Number(freshFrom.balance);
      const fromTotalDebit = amountFrom + fee;

      if (fromBefore < fromTotalDebit) throw new BadRequestError("Insufficient balance");

      const fromAfter = fromBefore - fromTotalDebit;
      const toBefore = Number(freshTo.balance);
      const toAfter = toBefore + toAmount;

      await freshFrom.update({ balance: fromAfter }, { transaction: t });
      await freshTo.update({ balance: toAfter }, { transaction: t });

      // Create 1 transaction record (your model supports swap kind)
      // direction = debit (because main walletId is from wallet). We store both wallet ids too.
      const trx = await Transaction.create(
        {
          userId: freshFrom.userId,
          source: "wallet",
          toType: "internal",
          kind: "swap",
          direction: "debit",
          currency: fromCurrency,
          amount: amountFrom,
          fee,
          feeCurrency: body?.feeCurrency || fromCurrency,
          walletId: freshFrom.id,
          fromWalletId: freshFrom.id,
          toWalletId: freshTo.id,
          tradePair: `${fromCurrency}/${toCurrency}`,
          price: rate,
          quantity: toAmount,
          balanceBefore: fromBefore,
          balanceAfter: fromAfter,
          status: body?.status || "completed",
          description: body?.description || "Wallet swap",
          notes: body?.notes || null,
          metadata: {
            ...(body?.metadata || {}),
            adminId: adminId || null,
            toCurrency,
            toAmount,
            toBalanceBefore: toBefore,
            toBalanceAfter: toAfter,
          },
        },
        { transaction: t }
      );

      return {
        fromWallet: freshFrom,
        toWallet: freshTo,
        transaction: trx,
      };
    });
  }
}

module.exports = new adminWalletService();