// services/FundingService.js
const { Op } = require("sequelize");
const { Transaction, PaymentMethod, WithdrawalMethod, sequelize, Network, Wallet, CoinNetwork, UserBalance, Asset, OTP, User } = require("../models");
const { BadRequestError, NotFoundError, ConflictError } = require("../utils/errors");
const { mapDepositMethod, generateTransactionId, mapWithdrawalMethod, safeNumber } = require("../utils/helpers");
const logger = require("../utils/logger");
const otpService = require("../services/otpService")
const speakeasy = require("speakeasy")

function generateTxId(prefix = "FD") {
  // FD-20260210-8F3K1ZP9Q2
  const rand = Math.random().toString(36).slice(2, 12).toUpperCase();
  const date = new Date().toISOString().slice(0, 10).replaceAll("-", "");
  return `${prefix}-${date}-${rand}`;
}

// pass in upload result (url/path) from your controller after multer upload
class FundingService {

  async getDeposit() {
    const methods = await PaymentMethod.findAll({
      where: { isActive: true }
    })

    return {
      methods: methods.map(m => mapDepositMethod(m))
    }
  }

  async payDeposit(userId, transactionId) {

    const transaction = await Transaction.findOne({
      where: {
        userId,
        transactionId,
        status: {
          [Op.in]: ['initiated', 'pending', 'processing']
        }
      }
    })

    if (!transaction) {
      throw new ConflictError('Transaction does not exists')
    }
    let transactionMethod = {};

    try {
      transactionMethod =
        typeof transaction.metadata === "string"
          ? JSON.parse(transaction.metadata)
          : transaction.metadata || {};
    } catch (e) {
      console.error("Invalid JSON:", transaction.metadata);
    }
    const method = await PaymentMethod.findOne({
      where: {
        id: transactionMethod?.methodId ?? null
      }
    })

    if (!method) {
      throw new ConflictError('Invalid Method')
    }


    return {
      transaction, method: mapDepositMethod(method), details: JSON.parse(method.details)
    }
  }
  async uploadDeposit(userId, data) {
    const { transactionId,
      fileUrl,
      fileName,
      mimeType,
      fileSize,
    } = data

    if (!fileUrl) {
      throw new BadRequestError('fileUrl is required');
    }
    const transaction = await Transaction.findOne({
      where: {
        userId,
        transactionId,
        status: {
          [Op.in]: ['initiated', 'pending', 'processing']
        }
      }
    })

    if (!transaction) {
      throw new ConflictError('Transaction does not exists')
    }

    /* const metadata = transaction.metadata ?? {};
      await transaction.update({
        metadata: {
          ...metadata,
          proof: fileName,
          proof_url: fileUrl
        },
        status: 'pending',
      }) */
    const updatedTransaction = await sequelize.transaction(async (t) => {

      const metadata = transaction.metadata ?? {};
      await transaction.update({
        metadata: {
          ...metadata,
          proof: fileName,
          proof_url: fileUrl
        },
        status: 'pending',
      }, {
        transaction: t
      })

      return transaction;
    })

    return {
      transaction: updatedTransaction
    }
  }

  async startDeposit(userId, currency, data) {
    const { amount, method: methodId } = data;

    const currentAmount = Number(amount);
    if (!Number.isFinite(currentAmount) || currentAmount <= 0) {
      throw new BadRequestError("Amount must be a positive number");
    }

    if (!methodId) {
      throw new BadRequestError("methodId is required");
    }

    // 1) Validate method exists + active
    const method = await PaymentMethod.findOne({
      where: { reference: methodId, isActive: true }, // reference = public id from frontend
    });

    if (!method) {
      throw new NotFoundError("Payment method not found or inactive");
    }

    // 2) Enforce min/max if you store them on method
    const min = Number(method.minimum ?? 0);
    const max = Number(method.max ?? 0);

    if (Number.isFinite(min) && min > 0 && currentAmount < min) {
      throw new BadRequestError(`Minimum deposit for ${method.name} is ${min}`);
    }
    if (Number.isFinite(max) && max > 0 && currentAmount > max) {
      throw new BadRequestError(`Maximum deposit for ${method.name} is ${max}`);
    }

    // 3) Prevent duplicates / spam (include method in filter)
    // Best practice: store methodRef in a column or in metadata and filter by it
    const existing = await Transaction.findOne({
      where: {
        userId,
        source: "funding",
        kind: "deposit",
        status: { [Op.in]: ["pending", "initiated", "processing", "under_review"] },
        // If amount duplicates are common, include amount; otherwise remove it
        amount: currentAmount,
        // If your DB supports JSON querying, filter metadata too; otherwise add a dedicated column
        // metadata: { methodRef: methodId },
      },
      order: [["created_at", "DESC"]],
    });

    if (existing) {
      throw new BadRequestError(
        `You already have a pending deposit of ${currentAmount} via ${method.name}. Complete it or cancel it.`
      );
    }

    // 4) Create transaction
    const tx = await Transaction.create({
      transactionId: generateTransactionId("FD"),
      userId,
      source: "funding",
      toType: "internal",
      kind: "deposit",
      direction: "credit",
      currency,
      amount: currentAmount,

      fee: 0,
      feeCurrency: null,

      status: "initiated",
      description: `Funding deposit via ${method.name}`,

      metadata: {
        methodId: method.id, // internal PK
        methodRef: method.reference, // public id used by frontend
        methodName: method.name || null,
      },
    });

    return tx;
  }

  async cancelDeposit(userId, transactionId) {
    const tx = await Transaction.findOne({
      where: {
        userId,
        transactionId,
        source: "funding",
        kind: "deposit",
      },
    });
    if (!tx) {
      throw new NotFoundError("Funding deposit transaction not found");
    }
    await tx.update({
      status: 'cancelled',
      cancelledAt: new Date()
    })
    return { transaction: tx }
  }

  async submitFundingProof(userId, payload) {
    const { transactionId, proofUrl, amountUSD, currency, address, network } = payload;

    // 1) Find the transaction
    const tx = await Transaction.findOne({
      where: {
        userId,
        transactionId,
        source: "funding",
        kind: "deposit",
      },
    });

    if (!tx) {
      throw new NotFoundError("Funding deposit transaction not found");
    }

    // 2) Prevent resubmitting proof
    const existingProof = tx.metadata?.payment_proof || tx.metadata?.proofUrl;
    if (existingProof) {
      throw new BadRequestError("Proof already submitted for this transaction");
    }

    // 3) Only allow proof on valid statuses
    const allowed = ["pending", "initiated", "processing", "under_review"];
    if (!allowed.includes(tx.status)) {
      throw new BadRequestError(`Cannot submit proof when status is '${tx.status}'`);
    }

    // 4) Update tx metadata + move status to under_review
    const newMeta = {
      ...(tx.metadata || {}),
      payment_proof: proofUrl,
      submitted_at: new Date().toISOString(),
      ui: {
        amountUSD,
        currency,
        address,
        network,
      },
    };

    await tx.update({
      status: "under_review",
      metadata: newMeta,
      processedAt: new Date(),
    });

    return tx;
  }

  async getWithdrawal(userId) {

    const [methods, wallets, networks, user] = await Promise.all([
      WithdrawalMethod.findAll({
        where: { isActive: true }
      }),
      Wallet.findAll({
        where: { userId },
        include: [
          {
            model: Asset,
            as: "coin",
            include: [{ model: CoinNetwork, as: "networks" }],
          },
        ],
      }),
      Network.findAll(),
      User.findOne({
        where: { userId }
      })
    ]);

    // Assets list (unique coins)
    const assetsMap = new Map();
    for (const w of wallets) {
      const coinId = w.coin_id ?? w.coinId ?? w.coin?.id;
      if (!coinId) continue;

      if (!assetsMap.has(coinId)) {
        assetsMap.set(coinId, {
          id: coinId,
          name: w.coin?.name ?? null,
          symbol: w.coin?.symbol ?? null,
          networks: (w.coin?.networks ?? []).map((cn) => ({
            id: cn.id,
            networkId: cn.network_id ?? cn.networkId ?? null,
            network: cn.network ?? cn.name ?? null,
            address: w.address ?? w.wallet_address ?? null, // adjust to your column
            memo: w.memo ?? null,
            minDeposit: cn.min_deposit ?? cn.minDeposit ?? null,
            confirmations: cn.confirmations ?? null,
            fee: cn.fee ?? cn.network_fee ?? null,
          })),
        });
      }
    }
    const assets = [...assetsMap.values()];

    // Network details (global)
    const networkDetails = networks.map((n) => ({
      id: n.id,
      name: n.name,
      network: n.network,
      confirmations: n.confirmations,
      minDeposit: n.minDeposits ?? n.min_deposit ?? null,
      estimatedTime: n.time ?? "30-60 min",
    }));


    return {
      methods: methods.map(m => mapWithdrawalMethod(m)),
      assets,
      networkDetails,
      security: {
        twoFactorMethod: user?.twoFactorMethod
      }
    }
  }


  async confirmWithdrawal(userId, currency, data) {
    const {
      amount,
      methodId, // only required for EXTERNAL
      type, // "wallet" | "external"
      symbol, // only required for WALLET
      network: networkInput, // only required for WALLET
      details, // only required for EXTERNAL (address)
      otp, // required for both (based on your UI)
    } = data;

    const user = await User.findByPk(userId);

    if (!user) {
      throw new NotFoundError('User not found');
    }

    // 0) Basic validation
    const currentAmount = Number(amount);
    if (!Number.isFinite(currentAmount) || currentAmount <= 0) {
      throw new BadRequestError("Amount must be a positive number");
    }

    if (!type || !["wallet", "external"].includes(type)) {
      throw new BadRequestError("Invalid withdrawal type");
    }

    // 1) OTP check (you have OTP in UI, so enforce it here)
    // Adjust to your real OTP service/table
    if (!otp || String(otp).trim().length === 0) {
      throw new BadRequestError("OTP is required");
    }
    // Example: await OtpService.verify({ userId, type: "withdrawal", code: String(otp).trim() });

    // 2) Type-specific “requirements”
    if (type === "external") {
      if (!methodId) throw new BadRequestError("Withdrawal method is required");
      if (!details || !String(details).trim()) {
        throw new BadRequestError("Withdrawal address is required");
      }
    }

    if (type === "wallet") {
      if (!symbol || !String(symbol).trim()) {
        throw new BadRequestError("Asset symbol is required for wallet withdrawal");
      }
      if (!networkInput || !String(networkInput).trim()) {
        throw new BadRequestError("Network is required for wallet withdrawal");
      }
    }
    const twofactor = user.twofactorEnabled ? user.twoFactorMethod : "email";
  
    
    if (twofactor === 'authenticator') {

      if (!user.twoFactorSecret) {
        throw new BadRequestError('Please setup 2FA first');
      }

      const isValid = speakeasy.totp.verify({
        secret: user.twoFactorSecret,
        encoding: 'base32',
        token: code,
        window: 1,
      });

      if (!isValid) {
        throw new BadRequestError('Invalid 2FA code');
      }

    }


    // =========================
    // EMAIL OTP FLOW
    // =========================
    if (twofactor === 'email') {
      try {
        const otpres = await otpService.verifyOTP(userId, "withdrawal", otp);
        if (!otpres?.status) {
          throw new BadRequestError('Invalid 2FA code');
        }
      } catch (error) {
        throw new BadRequestError("Invalid OTP or Expired")
      }
    }

    /* if(!OTP.verifyCode()) {
        throw new BadRequestError("Network is required for wallet withdrawal");
      } */

    // 3) Validate method ONLY if external
    let method = null;
    if (type === "external") {
      method = await WithdrawalMethod.findOne({
        where: { reference: methodId, isActive: true },
      });

      if (!method) {
        throw new NotFoundError("Withdrawal method not found or inactive");
      }

      // Optional: enforce method min/max
      const min = Number(method.minimum ?? 0);
      const max = Number(method.max ?? 0);

      if (Number.isFinite(min) && min > 0 && currentAmount < min) {
        throw new BadRequestError(`Minimum withdrawal is ${min}`);
      }
      if (Number.isFinite(max) && max > 0 && currentAmount > max) {
        throw new BadRequestError(`Maximum withdrawal is ${max}`);
      }
    }

    // 4) Prevent duplicates / spam (separate filters by type)
    const existingWhere = {
      userId,
      source: "funding",
      kind: "withdrawal",
      amount: currentAmount,
      status: { [Op.in]: ["pending", "initiated", "processing", "under_review"] },
    };

    // If you store method/symbol/network in metadata, filter by it:
    if (type === "external") {
      existingWhere.metadata = { methodRef: methodId }; // adjust to your JSON query support / schema
    } else {
      existingWhere.metadata = { symbol, network: networkInput };
    }

    const existing = await Transaction.findOne({
      where: existingWhere,
      order: [["created_at", "DESC"]],
    });

    if (existing) {
      throw new BadRequestError(
        `You already have a pending withdrawal of ${currentAmount}. Complete it or cancel it.`
      );
    }

    const result = await sequelize.transaction(async (t) => {
      // 5) Lock funding balance
      const balance = await UserBalance.findOne({
        where: { userId },
        transaction: t,
        lock: t.LOCK.UPDATE,
      });
      if (!balance) throw new ConflictError("User must have a user balance");

      const currentBalance = Number(balance.balance ?? 0);
      if (currentBalance < currentAmount) throw new ConflictError("Insufficient Balance");

      // 6) Wallet branch (internal transfer)
      let walletId = null;
      let coin = null;
      let networkRow = null;

      if (type === "wallet") {
        coin = await Asset.findOne({
          where: { symbol, status: true },
          transaction: t,
        });
        if (!coin) throw new ConflictError(`${symbol} is not available`);

        /**
         * IMPORTANT: In your frontend you set network to a networkId.
         * So DON'T search by reference unless your UI sends reference.
         * Use `id: networkInput` if it's the PK/UUID.
         */
        networkRow = await Network.findOne({
          where: { network: networkInput, status: true }, // <-- KEY FIX
          transaction: t,
        });
        if (!networkRow) throw new ConflictError(`${networkInput} network is not available`);

        const wallet = await Wallet.findOne({
          where: { userId, coinId: coin.id },
          transaction: t,
          lock: t.LOCK.UPDATE,
        });
        if (!wallet) throw new ConflictError(`${symbol} does not have a valid wallet`);

        walletId = wallet.id;

        // If "wallet withdrawal" means funding -> internal wallet credit, do it explicitly:
        // await wallet.update(
        //   { balance: Number(wallet.balance ?? 0) + currentAmount },
        //   { transaction: t }
        // );
      }

      // 7) External branch (pending) vs wallet branch (completed)
      const isWallet = type === "wallet";
      const status = isWallet ? "completed" : "pending";
      const toType = isWallet ? "internal" : "external";

      const description = isWallet
        ? `Internal transfer: funding → ${coin?.symbol} wallet`
        : `Withdrawal to ${method?.name} from funding account`;

      // 8) Create transaction
      const tx = await Transaction.create(
        {
          transactionId: generateTransactionId("WD"),
          userId,
          source: "funding",
          toType,
          kind: "withdrawal",
          direction: "debit",
          currency,
          walletId,
          amount: currentAmount,
          fee: 0,
          netAmount: currentAmount,

          // For wallet flow, store networkId; for external, you can store blank or method network if you have it
          network: isWallet ? String(networkInput) : "",
          details: isWallet ? "" : String(details ?? ""),

          status,
          description,
          metadata: {
            type,
            // external
            methodId: method?.id ?? null,
            methodRef: method?.reference ?? null,
            methodName: method?.name ?? null,
            // wallet
            symbol: isWallet ? symbol : null,
            networkId: isWallet ? networkRow?.id ?? String(networkInput) : null,
          },
        },
        { transaction: t }
      );

      // 9) Debit funding balance
      await balance.update({ balance: currentBalance - currentAmount }, { transaction: t });

      return tx;
    });

    logger.info(`Withdrawal requested: ${result.transactionId} by user ${userId}`);

    const recents = await Transaction.findAll({
      where: { userId, source: "funding", kind: "withdrawal" },
      order: [["created_at", "DESC"]],
      limit: 5,
    });

    return { transaction: result };
  }

  async getHistory(userId) {

    const transactions = await Transaction.findAll({
      where: { userId, source:'funding' },
      order: [['createdAt', 'DESC']]
    });

    const stats = await Transaction.findOne({
      where: { userId },
      attributes: [
        [sequelize.fn("COUNT", sequelize.col("id")), "totalTransactions"],

        [
          sequelize.literal(`
        COALESCE(SUM(CASE
          WHEN kind = 'deposit' AND status = 'completed' THEN amount
          ELSE 0
        END), 0)
      `),
          "totalDeposits",
        ],

        [
          sequelize.literal(`
        COALESCE(SUM(CASE
          WHEN kind = 'withdrawal' AND status = 'completed' THEN amount
          ELSE 0
        END), 0)
      `),
          "totalWithdrawals",
        ],

        [
          sequelize.literal(`
        COALESCE(SUM(CASE
          WHEN kind = 'deposit' AND status IN ('initiated', 'pending') THEN amount
          ELSE 0
        END), 0)
      `),
          "pendingDeposits",
        ],

        [
          sequelize.literal(`
        COALESCE(SUM(CASE
          WHEN kind = 'withdrawal' AND status IN ('initiated', 'pending') THEN amount
          ELSE 0
        END), 0)
      `),
          "pendingWithdrawals",
        ],
      ],
      raw: true,
    });

    return {
      transactions: transactions.map(t => ({
        transactionId: t.transactionId,
        type: t.kind,
        amount: t.netAmount,
        status: t.status,
        description: t.description,
        side: t.direction,
        date: t.createdAt
      })),
      stats,
      transactionTypes: ['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'],
      statusTypes: ['initiated', 'pending', 'processing', 'completed', 'failed', 'cancelled']
    };
  }
}

module.exports = new FundingService();

