import mongoose from "mongoose";

import * as identityHelpers from "$helpers/identity/identity.helper.js";
import * as paymentCardHelpers from "$helpers/payment/paymentCard.helper.js";
import models from "$models/index.js";
import { tadbirServices } from "$services/index.js";

const REFERRAL_INVITER_POINTS = 10000;
const REFERRAL_INVITEE_WELCOME_POINTS = 5000;
const FIRST_INVESTMENT_POINTS = 10000;
const REFERRAL_INVESTMENT_SHARE_RATE = 0.1;
const POINT_EXPIRY_DAYS = 365;
const ACCRUAL_LOOKBACK_DAYS = 190;
const DAILY_ACCRUAL_RATE = 0.004;
const HOLDING_3M_RATE = 0.005;
const HOLDING_6M_RATE = 0.006;
const MS_IN_DAY = 24 * 60 * 60 * 1000;
const INVITER_SHARE_SOURCES = new Set([
  "first_investment",
  "daily_investment",
  "holding_3m",
  "holding_6m",
]);

const parseInteger = (value, fallback = 0) => {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? Math.round(parsed) : fallback;
};

const parseNumericValue = (value) => {
  if (typeof value === "number") {
    return Number.isFinite(value) ? value : 0;
  }

  if (typeof value === "string") {
    const normalizedValue = value.replaceAll(",", "").trim();
    const parsedValue = Number(normalizedValue);
    return Number.isFinite(parsedValue) ? parsedValue : 0;
  }

  return 0;
};

const normalizeDate = (value, fallback = new Date()) => {
  const date = value ? new Date(value) : new Date(fallback);
  return Number.isNaN(date.getTime()) ? new Date(fallback) : date;
};

const addDays = (date, days) => {
  const nextDate = new Date(date);
  nextDate.setDate(nextDate.getDate() + days);
  return nextDate;
};

const startOfDay = (date) => {
  const nextDate = new Date(date);
  nextDate.setHours(0, 0, 0, 0);
  return nextDate;
};

const endOfDay = (date) => {
  const nextDate = new Date(date);
  nextDate.setHours(23, 59, 59, 999);
  return nextDate;
};

const diffInDays = (leftDate, rightDate) =>
  Math.max(
    0,
    Math.floor(
      (startOfDay(normalizeDate(leftDate)) - startOfDay(normalizeDate(rightDate))) /
        MS_IN_DAY,
    ),
  );

const buildExpiryDate = (baseDate = new Date()) =>
  endOfDay(addDays(baseDate, POINT_EXPIRY_DAYS));

const normalizeReferralCode = (value = "") =>
  String(value || "")
    .trim()
    .toUpperCase()
    .replace(/[^A-Z0-9]/g, "");

const buildDayKey = (date = new Date()) =>
  startOfDay(date).toISOString().slice(0, 10);

const buildReferralCodeCandidate = (user = {}) => {
  const identifier = identityHelpers.getUserIdentifier(user);
  const suffix = identifier.slice(-4) || String(user?._id || "").slice(-4);
  const random = Math.random().toString(36).slice(2, 6).toUpperCase();
  return normalizeReferralCode(`OMI${suffix}${random}`);
};

const resolvePositiveTransactionType = (source = "manual") => {
  if (source === "manual_adjustment") {
    return "adjusted";
  }

  if (source === "reward_redemption_refund") {
    return "reversed";
  }

  return "earned";
};

const allocatePointsForSpend = async (userId, spendAmount, now = new Date()) => {
  const credits = await models.clubPointTransaction
    .find({
      user: userId,
      remainingPoints: { $gt: 0 },
      $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }],
    })
    .sort({
      expiresAt: 1,
      createdAt: 1,
      _id: 1,
    });

  let remaining = spendAmount;
  const allocations = [];

  for (const credit of credits) {
    if (remaining <= 0) {
      break;
    }

    const available = parseInteger(credit.remainingPoints);
    if (available <= 0) {
      continue;
    }

    const consumed = Math.min(available, remaining);
    credit.remainingPoints = available - consumed;
    await credit.save();

    allocations.push({
      transactionId: credit._id,
      points: consumed,
      expiresAt: credit.expiresAt || null,
    });

    remaining -= consumed;
  }

  if (remaining > 0) {
    throw {
      status: 422,
      message: "امتیاز کافی برای این عملیات وجود ندارد.",
    };
  }

  return allocations;
};

const getSpendReversalExpiry = (allocations = [], fallbackDate = new Date()) => {
  const expiryCandidates = allocations
    .map((allocation) => normalizeDate(allocation?.expiresAt, null))
    .filter((value) => value instanceof Date && !Number.isNaN(value.getTime()))
    .sort((left, right) => left.getTime() - right.getTime());

  return expiryCandidates[0] || buildExpiryDate(fallbackDate);
};

export const ensureUserReferralCode = async (userOrId) => {
  const user =
    userOrId?._id && userOrId.clubReferralCode !== undefined
      ? userOrId
      : await models.user.findById(userOrId);

  if (!user) {
    return null;
  }

  if (String(user.clubReferralCode || "").trim()) {
    return normalizeReferralCode(user.clubReferralCode);
  }

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const candidate = buildReferralCodeCandidate(user);
    const existing = await models.user
      .findOne({ clubReferralCode: candidate })
      .select("_id")
      .lean();

    if (!existing) {
      user.clubReferralCode = candidate;
      await user.save();
      return candidate;
    }
  }

  const fallbackCode = normalizeReferralCode(
    `${String(user._id).slice(-6)}${Date.now().toString(36).toUpperCase()}`,
  );
  user.clubReferralCode = fallbackCode;
  await user.save();
  return fallbackCode;
};

export const getUserPointBalance = async (userId) => {
  const rows = await models.clubPointTransaction.aggregate([
    {
      $match: {
        user: new mongoose.Types.ObjectId(userId),
      },
    },
    {
      $group: {
        _id: null,
        total: { $sum: "$points" },
      },
    },
  ]);

  return parseInteger(rows[0]?.total);
};

export const createPointCredit = async ({
  userId,
  points,
  source,
  sourceId = null,
  eventKey = "",
  meta = {},
  expiresAt,
  occurredAt = new Date(),
}) => {
  const normalizedPoints = parseInteger(points);

  if (normalizedPoints <= 0) {
    return null;
  }

  if (eventKey) {
    const existing = await models.clubPointTransaction.findOne({ eventKey });
    if (existing) {
      return existing;
    }
  }

  const createdAt = normalizeDate(occurredAt);
  const expiryDate = expiresAt ? normalizeDate(expiresAt) : buildExpiryDate(createdAt);

  const transaction = await models.clubPointTransaction.create({
    user: userId,
    type: resolvePositiveTransactionType(source),
    points: normalizedPoints,
    originalPoints: normalizedPoints,
    remainingPoints: normalizedPoints,
    source,
    sourceId,
    eventKey,
    meta,
    expiresAt: expiryDate,
    createdAt,
    updatedAt: createdAt,
  });

  if (INVITER_SHARE_SOURCES.has(source)) {
    const creditedUser = await models.user
      .findById(userId)
      .select("_id clubInviter")
      .lean();
    const inviterSharePoints = parseInteger(
      normalizedPoints * REFERRAL_INVESTMENT_SHARE_RATE,
    );

    if (creditedUser?.clubInviter && inviterSharePoints > 0) {
      await createPointCredit({
        userId: creditedUser.clubInviter,
        points: inviterSharePoints,
        source: "referral_investment_share",
        sourceId: transaction._id,
        eventKey: eventKey
          ? `${eventKey}:inviter-share`
          : `club:inviter-share:${transaction._id}`,
        meta: {
          inviteeUserId: userId,
          sourceTransactionId: transaction._id,
          source,
        },
        expiresAt: expiryDate,
        occurredAt: createdAt,
      });
    }
  }

  return transaction;
};

export const createSpendTransaction = async ({
  userId,
  points,
  source,
  sourceId = null,
  eventKey = "",
  meta = {},
  occurredAt = new Date(),
}) => {
  const normalizedPoints = parseInteger(points);

  if (normalizedPoints <= 0) {
    throw {
      status: 422,
      message: "مقدار امتیاز باید بیشتر از صفر باشد.",
    };
  }

  if (eventKey) {
    const existing = await models.clubPointTransaction.findOne({ eventKey });
    if (existing) {
      return existing;
    }
  }

  const allocations = await allocatePointsForSpend(
    userId,
    normalizedPoints,
    normalizeDate(occurredAt),
  );
  const createdAt = normalizeDate(occurredAt);

  return models.clubPointTransaction.create({
    user: userId,
    type: "spent",
    points: -normalizedPoints,
    originalPoints: 0,
    remainingPoints: 0,
    source,
    sourceId,
    eventKey,
    meta: {
      ...meta,
      allocations,
    },
    expiresAt: null,
    createdAt,
    updatedAt: createdAt,
  });
};

export const reverseSpendTransaction = async (
  spendTransactionOrId,
  { source = "reward_redemption_refund", sourceId = null, eventKey = "", meta = {} } = {},
) => {
  const spendTransaction =
    spendTransactionOrId?._id && spendTransactionOrId.points !== undefined
      ? spendTransactionOrId
      : await models.clubPointTransaction.findById(spendTransactionOrId);

  if (!spendTransaction) {
    return null;
  }

  if (eventKey) {
    const existing = await models.clubPointTransaction.findOne({ eventKey });
    if (existing) {
      return existing;
    }
  }

  const points = Math.abs(parseInteger(spendTransaction.points));
  if (points <= 0) {
    return null;
  }

  return createPointCredit({
    userId: spendTransaction.user,
    points,
    source,
    sourceId: sourceId || spendTransaction._id,
    eventKey,
    meta: {
      ...meta,
      reversedSpendTransactionId: spendTransaction._id,
    },
    expiresAt: getSpendReversalExpiry(
      spendTransaction.meta?.allocations || [],
      spendTransaction.createdAt,
    ),
  });
};

export const awardReferralQualification = async (referralOrId) => {
  const referral =
    referralOrId?._id && referralOrId.inviter
      ? referralOrId
      : await models.clubReferral.findById(referralOrId);

  if (!referral) {
    return null;
  }

  if (referral.status === "qualified") {
    return referral;
  }

  referral.status = "qualified";
  referral.qualifiedAt = new Date();
  await referral.save();

  await Promise.all([
    createPointCredit({
      userId: referral.inviter,
      points: REFERRAL_INVITER_POINTS,
      source: "referral_inviter_bonus",
      sourceId: referral._id,
      eventKey: `club:referral:inviter:${referral._id}`,
      meta: {
        referralId: referral._id,
        referralCode: referral.referralCode,
      },
    }),
    createPointCredit({
      userId: referral.invitee,
      points: REFERRAL_INVITEE_WELCOME_POINTS,
      source: "referral_invitee_welcome",
      sourceId: referral._id,
      eventKey: `club:referral:invitee:${referral._id}`,
      meta: {
        referralId: referral._id,
        referralCode: referral.referralCode,
      },
    }),
  ]);

  const qualifiedCount = await models.clubReferral.countDocuments({
    inviter: referral.inviter,
    status: "qualified",
  });

  await models.user.findByIdAndUpdate(referral.inviter, {
    $set: {
      clubSuccessfulReferralCount: qualifiedCount,
    },
  });

  return referral;
};

export const assignReferralToUser = async ({ user, referralCode }) => {
  const normalizedCode = normalizeReferralCode(referralCode);
  if (!normalizedCode || !user?._id) {
    return null;
  }

  if (user.clubInviter) {
    throw {
      status: 409,
      message: "برای این کاربر قبلا معرف ثبت شده است.",
    };
  }

  const inviter = await models.user.findOne({ clubReferralCode: normalizedCode });
  if (!inviter) {
    throw {
      status: 404,
      message: "کد معرفی معتبر نیست.",
    };
  }

  if (String(inviter._id) === String(user._id)) {
    throw {
      status: 422,
      message: "استفاده از کد معرفی خود کاربر مجاز نیست.",
    };
  }

  const inviterIdentifier = identityHelpers.getUserIdentifier(inviter);
  const inviteeIdentifier = identityHelpers.getUserIdentifier(user);
  if (inviterIdentifier && inviteeIdentifier && inviterIdentifier === inviteeIdentifier) {
    throw {
      status: 422,
      message: "استفاده از کد معرفی خود کاربر مجاز نیست.",
    };
  }

  user.clubInviter = inviter._id;
  await user.save();

  return models.clubReferral.findOneAndUpdate(
    { invitee: user._id },
    {
      $set: {
        inviter: inviter._id,
        invitee: user._id,
        referralCode: normalizedCode,
        status: "pending",
      },
    },
    {
      new: true,
      upsert: true,
      setDefaultsOnInsert: true,
    },
  );
};

export const handleNewUserClubRegistration = async ({ user, referralCode = "" }) => {
  const freshUser =
    user?._id && user.clubReferralCode !== undefined
      ? user
      : await models.user.findById(user?._id || user);

  if (!freshUser) {
    return null;
  }

  await ensureUserReferralCode(freshUser);

  if (normalizeReferralCode(referralCode)) {
    const referral = await assignReferralToUser({
      user: freshUser,
      referralCode,
    });
    await awardReferralQualification(referral);
  }

  return freshUser;
};

export const awardFirstInvestmentPointsForRequest = async (requestOrId) => {
  const request =
    requestOrId?._id && requestOrId.investor
      ? requestOrId
      : await models.request.findById(requestOrId).lean();

  if (!request) {
    return null;
  }

  if (request.operationType !== "issuance" || request.status !== "approved") {
    return null;
  }

  const priorApprovedCount = await models.request.countDocuments({
    investor: request.investor,
    operationType: "issuance",
    status: "approved",
    _id: { $ne: request._id },
  });

  if (priorApprovedCount === 0) {
    await createPointCredit({
      userId: request.investor,
      points: FIRST_INVESTMENT_POINTS,
      source: "first_investment",
      sourceId: request._id,
      eventKey: `club:first-investment:${request.investor}`,
      meta: {
        requestId: request._id,
        trackId: request.trackId,
      },
      occurredAt: request.updatedAt || request.createdAt || new Date(),
    });
  }
  return null;
};

export const expireDuePoints = async ({ now = new Date() } = {}) => {
  const dueCredits = await models.clubPointTransaction.find({
    remainingPoints: { $gt: 0 },
    expiresAt: { $lte: now },
  });

  let expiredCount = 0;

  for (const credit of dueCredits) {
    const pointsToExpire = parseInteger(credit.remainingPoints);
    if (pointsToExpire <= 0) {
      continue;
    }

    const eventKey = `club:expire:${credit._id}:${buildDayKey(now)}`;
    const existing = await models.clubPointTransaction.findOne({ eventKey });
    if (existing) {
      credit.remainingPoints = 0;
      await credit.save();
      continue;
    }

    await models.clubPointTransaction.create({
      user: credit.user,
      type: "expired",
      points: -pointsToExpire,
      originalPoints: 0,
      remainingPoints: 0,
      source: "points_expiration",
      sourceId: credit._id,
      eventKey,
      meta: {
        expiredTransactionId: credit._id,
      },
      expiresAt: null,
    });

    credit.remainingPoints = 0;
    await credit.save();
    expiredCount += 1;
  }

  return expiredCount;
};

const fetchAllTurnoverRows = async ({
  nationalCode,
  fundSeoRegisterNo,
  fromDate,
  toDate,
}) => {
  const pageSize = 200;
  let pageNumber = 1;
  let totalPage = 1;
  const rows = [];

  while (pageNumber <= totalPage) {
    const response = await tadbirServices.getTurnoverAccount({
      nationalCode,
      fundSeoRegisterNo,
      fromDate,
      toDate,
      pageNumber,
      pageSize,
    });

    rows.push(...(response?.turnoverAccount || []));
    totalPage = Number(response?.totalPage) || 1;
    pageNumber += 1;
  }

  return rows;
};

const getAccrualRateForHoldingDays = (holdingDays) => {
  if (holdingDays >= 180) {
    return {
      rate: HOLDING_6M_RATE,
      source: "holding_6m",
    };
  }

  if (holdingDays >= 90) {
    return {
      rate: HOLDING_3M_RATE,
      source: "holding_3m",
    };
  }

  return {
    rate: DAILY_ACCRUAL_RATE,
    source: "daily_investment",
  };
};

const buildAccrualSnapshot = (rows = [], targetDate = new Date()) => {
  const normalizedRows = rows
    .map((row) => ({
      ...row,
      voucherDate: normalizeDate(row?.voucherDate),
      accountBalance: parseNumericValue(row?.accountBalance),
    }))
    .filter(
      (row) =>
        row.voucherDate instanceof Date && !Number.isNaN(row.voucherDate.getTime()),
    )
    .sort((left, right) => left.voucherDate - right.voucherDate);

  const targetEnd = endOfDay(targetDate);
  const effectiveRows = normalizedRows.filter((row) => row.voucherDate <= targetEnd);
  const latestRow = effectiveRows[effectiveRows.length - 1];

  if (!latestRow || latestRow.accountBalance <= 0) {
    return null;
  }

  let lastZeroOrNegativeIndex = -1;

  for (let index = 0; index < effectiveRows.length; index += 1) {
    if (effectiveRows[index].accountBalance <= 0) {
      lastZeroOrNegativeIndex = index;
    }
  }

  const holdingStartRow = effectiveRows[lastZeroOrNegativeIndex + 1] || latestRow;
  const holdingDays = diffInDays(targetEnd, holdingStartRow.voucherDate) + 1;
  const { rate, source } = getAccrualRateForHoldingDays(holdingDays);
  const points = parseInteger((latestRow.accountBalance / 365) * rate);

  if (points <= 0) {
    return null;
  }

  return {
    balanceAmount: latestRow.accountBalance,
    holdingDays,
    points,
    source,
  };
};

export const accrueDailyInvestmentPoints = async ({
  targetDate = addDays(new Date(), -1),
} = {}) => {
  const normalizedTargetDate = startOfDay(targetDate);
  const fromDate = startOfDay(addDays(normalizedTargetDate, -ACCRUAL_LOOKBACK_DAYS));
  const users = await models.user.find({
    isTadbirVerified: true,
    $or: [
      { nationalId: { $exists: true, $ne: "" } },
      { businessID: { $exists: true, $ne: "" } },
    ],
  });

  let createdTransactions = 0;

  for (const user of users) {
    const nationalCode = identityHelpers.getUserIdentifier(user);
    if (!nationalCode) {
      continue;
    }

    let balances = [];
    try {
      balances = await tadbirServices.getBalances(nationalCode);
    } catch (error) {
      console.error("[club] failed to fetch balances", {
        userId: user._id,
        error: error?.message || error,
      });
      continue;
    }

    for (const balance of balances || []) {
      const fundSeoRegisterNo = String(balance?.fundSeoRegisterNo || "").trim();
      if (!fundSeoRegisterNo) {
        continue;
      }

      try {
        const turnoverRows = await fetchAllTurnoverRows({
          nationalCode,
          fundSeoRegisterNo,
          fromDate: fromDate.toISOString(),
          toDate: endOfDay(normalizedTargetDate).toISOString(),
        });
        const accrual = buildAccrualSnapshot(turnoverRows, normalizedTargetDate);

        if (!accrual) {
          continue;
        }

        const eventKey = `club:accrual:${user._id}:${fundSeoRegisterNo}:${buildDayKey(normalizedTargetDate)}`;
        const existing = await models.clubPointTransaction.findOne({ eventKey });
        if (existing) {
          continue;
        }

        const transaction = await createPointCredit({
          userId: user._id,
          points: accrual.points,
          source: accrual.source,
          sourceId: fundSeoRegisterNo,
          eventKey,
          meta: {
            fundSeoRegisterNo,
            fundName: balance?.fundName || "",
            targetDate: normalizedTargetDate.toISOString(),
            balanceAmount: accrual.balanceAmount,
            holdingDays: accrual.holdingDays,
          },
          occurredAt: normalizedTargetDate,
        });

        if (transaction) {
          createdTransactions += 1;
        }
      } catch (error) {
        console.error("[club] failed to accrue balance", {
          userId: user._id,
          fundSeoRegisterNo,
          error: error?.message || error,
        });
      }
    }
  }

  return createdTransactions;
};

export const fulfillRewardRedemption = async ({ user, reward }) => {
  const rewardBatch =
    reward?.paymentBatch && typeof reward.paymentBatch === "object"
      ? reward.paymentBatch
      : await models.paymentBatchCard.findById(reward?.paymentBatch);
  const amount = parseInteger(rewardBatch?.amount);
  const fundSeoRegisterNo = String(rewardBatch?.fundSeoRegisterNo || "").trim();
  const fundName = String(rewardBatch?.fundName || "").trim();
  const expiresAt = rewardBatch?.expiresAt ? new Date(rewardBatch.expiresAt) : null;

  if (!amount || !fundSeoRegisterNo || !fundName || !expiresAt) {
    throw {
      status: 422,
      message: "تنظیمات جایزه برای صدور کارت هدیه کامل نیست.",
    };
  }

  const redemption = await models.clubRewardRedemption.create({
    user: user._id,
    reward: reward._id,
    pointsSpent: reward.pointsCost,
    status: "pending",
  });

  let spendTransaction = null;

  try {
    spendTransaction = await createSpendTransaction({
      userId: user._id,
      points: reward.pointsCost,
      source: "reward_redemption",
      sourceId: redemption._id,
      eventKey: `club:redeem:spend:${redemption._id}`,
      meta: {
        rewardId: reward._id,
      },
    });

    const batch = await models.paymentBatchCard.create({
      title: `بازخرید جایزه - ${fundName}`,
      description: "کارت هدیه صادرشده از جایزه باشگاه مشتریان",
      createMode: "single",
      usageContext: "distribution",
      quantity: 1,
      amount,
      fundSeoRegisterNo,
      fundName,
      totalAmount: amount,
      status: "active",
      user: user._id,
      expiresAt: endOfDay(expiresAt),
    });

    const [cardNumber] = await paymentCardHelpers.generateUniqueCardNumbers(1);
    const paymentCard = await models.paymentCard.create({
      batchId: batch._id,
      cardNumber,
        amount,
        fundSeoRegisterNo,
        fundName,
        status: "active",
        expiresAt: endOfDay(expiresAt),
      });

    redemption.status = "fulfilled";
    redemption.paymentCard = paymentCard._id;
    redemption.paymentBatch = batch._id;
    redemption.spendTransaction = spendTransaction?._id || null;
    redemption.fulfilledAt = new Date();
    redemption.meta = {
      ...redemption.meta,
      cardNumber: paymentCard.cardNumber,
      fundSeoRegisterNo,
      fundName,
      amount,
    };
    await redemption.save();

    return redemption;
  } catch (error) {
    let reversalTransaction = null;

    if (spendTransaction?._id) {
      reversalTransaction = await reverseSpendTransaction(spendTransaction, {
        sourceId: redemption._id,
        eventKey: `club:redeem:refund:${redemption._id}`,
        meta: {
          rewardId: reward._id,
        },
      });
    }

    redemption.status = "failed";
    redemption.spendTransaction = spendTransaction?._id || null;
    redemption.reversalTransaction = reversalTransaction?._id || null;
    redemption.meta = {
      ...redemption.meta,
      failureMessage: error?.message || "reward fulfillment failed",
    };
    await redemption.save();
    throw error;
  }
};

export const getUserClubSnapshot = async (userOrId) => {
  const user =
    userOrId?._id && userOrId.clubReferralCode !== undefined
      ? userOrId
      : await models.user.findById(userOrId);

  if (!user) {
    return null;
  }

  const referralCode = await ensureUserReferralCode(user);
  const [currentPoints, successfulReferralsCount, recentRewardRedemptions] =
    await Promise.all([
      getUserPointBalance(user._id),
      models.clubReferral.countDocuments({
        inviter: user._id,
        status: "qualified",
      }),
      models.clubRewardRedemption
        .find({
          user: user._id,
        })
        .populate("paymentCard")
        .populate({
          path: "reward",
          populate: {
            path: "paymentBatch",
          },
        })
        .sort({ createdAt: -1 })
        .limit(5)
        .lean(),
    ]);

  return {
    currentPoints,
    successfulReferralsCount,
    referralCode,
    clubInviter: user.clubInviter || null,
    latestRedemptions: recentRewardRedemptions,
  };
};
