import models from "$models/index.js";
import { redisConnection } from "$connections/index.js";
import { servicesConfig } from "$config/index.js";
import {
  identityHelpers,
  paymentHelpers,
  sepahVerificationHelpers,
} from "$helpers/index.js";
import { sepahBrokerServices, tadbirServices } from "$services/index.js";
import * as RequestController from "$controllers/request/request.controller.js";
import {
  isSepahBrokerFund,
  SEPAH_BROKER_EXTERNAL_SOURCE,
} from "$services/sepah/sepah-broker.helper.js";

import { telegramBotConfig } from "./config.js";

const MAX_ISSUANCE_AMOUNT_RIAL = 4_000_000_000;
const DISABLED_FUND_CODES = new Set(["11339"]);
const TELEGRAM_FUNDS_CACHE_KEY = "telegram:bot:funds:last-known-good";
const TELEGRAM_FUNDS_CACHE_TTL_SECONDS = 5 * 60;
const TELEGRAM_FUNDS_REQUEST_TIMEOUT_MS = 10_000;
const STANDARD_GATEWAY_FUND_KEYS = Object.freeze({
  11141: "NAVID",
  11383: "OMID",
  11343: "GANJINEH",
});

const parseNumber = (value) => {
  const number = Number(value);
  return Number.isFinite(number) ? number : 0;
};

const getWageSetting = (fund, name) =>
  parseNumber(fund?.wageSettings?.find((item) => item?.name === name)?.value);

const calculateVariableWage = (amountRial, rate, maximum) => {
  if (amountRial <= 0 || rate <= 0) {
    return 0;
  }

  const wage = Math.round(amountRial * (rate / 100));
  return maximum > 0 ? Math.min(wage, maximum) : wage;
};

const resolveFundCode = (fund) => String(fund?.fundRegisterCode || "").trim();
const resolveFundSeoRegisterNo = (fund) =>
  String(fund?.nav?.seoRegisterNo || fund?.fundRegisterCode || "").trim();

const resolveGatewayFundKey = (fundCode) => {
  const normalizedCode = String(fundCode || "").trim();
  return isSepahBrokerFund(normalizedCode)
    ? normalizedCode
    : STANDARD_GATEWAY_FUND_KEYS[normalizedCode] || "";
};

const isSupportedFund = (fund) => {
  const code = resolveFundCode(fund);
  return (
    Boolean(code) &&
    !DISABLED_FUND_CODES.has(code) &&
    (isSepahBrokerFund(code) || Boolean(STANDARD_GATEWAY_FUND_KEYS[code]))
  );
};

const normalizeFundItems = (response) => {
  if (Array.isArray(response)) {
    return response;
  }

  for (const key of ["items", "funds", "data"]) {
    if (Array.isArray(response?.[key])) {
      return response[key];
    }
  }

  return [];
};

const withTimeout = async (promise, timeoutMs, message) => {
  let timeoutId;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      const error = new Error(message);
      error.code = "TELEGRAM_FUNDS_TIMEOUT";
      reject(error);
    }, timeoutMs);
  });

  try {
    return await Promise.race([promise, timeoutPromise]);
  } finally {
    clearTimeout(timeoutId);
  }
};

const readCachedTelegramFunds = async () => {
  try {
    const payload = await redisConnection.get(TELEGRAM_FUNDS_CACHE_KEY);
    if (!payload) {
      return [];
    }

    const funds = JSON.parse(payload);
    return Array.isArray(funds) ? funds.filter(isSupportedFund) : [];
  } catch (error) {
    console.warn(
      "[telegram-bot] unable to read the fund cache:",
      error?.message || "unknown error",
    );
    return [];
  }
};

const cacheTelegramFunds = async (funds) => {
  try {
    await redisConnection.setex(
      TELEGRAM_FUNDS_CACHE_KEY,
      TELEGRAM_FUNDS_CACHE_TTL_SECONDS,
      JSON.stringify(funds),
    );
  } catch (error) {
    console.warn(
      "[telegram-bot] unable to update the fund cache:",
      error?.message || "unknown error",
    );
  }
};

export const getTelegramFunds = async ({ allowCached = true } = {}) => {
  if (allowCached) {
    const cachedFunds = await readCachedTelegramFunds();
    if (cachedFunds.length > 0) {
      return cachedFunds;
    }
  }

  try {
    const response = await withTimeout(
      tadbirServices.getFunds(),
      TELEGRAM_FUNDS_REQUEST_TIMEOUT_MS,
      "دریافت اطلاعات صندوق‌ها بیش از حد مجاز زمان برد.",
    );
    const funds = normalizeFundItems(response).filter(isSupportedFund);
    await cacheTelegramFunds(funds);
    return funds;
  } catch (error) {
    console.error("[telegram-bot] Tadbir fund request failed:", {
      code: error?.code || "",
      message: error?.message || "unknown error",
    });

    if (allowCached) {
      const cachedFunds = await readCachedTelegramFunds();
      if (cachedFunds.length > 0) {
        console.warn("[telegram-bot] using last-known-good fund data");
        return cachedFunds;
      }
    }

    const serviceError = new Error(
      "سرویس اطلاعات صندوق‌ها موقتاً در دسترس نیست. لطفاً کمی بعد دوباره تلاش کنید.",
    );
    serviceError.code = error?.code || "TELEGRAM_FUNDS_UNAVAILABLE";
    serviceError.cause = error;
    throw serviceError;
  }
};

export const getTelegramFund = async (
  fundCode,
  { allowCached = true } = {},
) => {
  const normalizedCode = String(fundCode || "").trim();
  const funds = await getTelegramFunds({ allowCached });
  return funds.find((fund) => resolveFundCode(fund) === normalizedCode) || null;
};

const validateIssuanceQuote = ({ fund, units, amountRial }) => {
  if (!Number.isSafeInteger(amountRial) || amountRial < 1000) {
    throw new Error("مبلغ نهایی صدور باید معتبر باشد.");
  }

  if (amountRial > MAX_ISSUANCE_AMOUNT_RIAL) {
    throw new Error(
      `حداکثر مبلغ مجاز برای صدور ${MAX_ISSUANCE_AMOUNT_RIAL.toLocaleString("fa-IR")} ریال است.`,
    );
  }

  const minUnits = Math.max(0, parseNumber(fund?.minimumCapitalUnit));
  const maxUnits = Math.max(0, parseNumber(fund?.maximumCapitalUnit));
  const remainedUnits = Math.max(0, parseNumber(fund?.remainedUnit));

  if (!Number.isSafeInteger(units) || units <= 0) {
    throw new Error("تعداد واحد صدور باید معتبر باشد.");
  }

  if (remainedUnits <= 0) {
    throw new Error(
      "در حال حاضر واحد قابل صدور برای این صندوق باقی نمانده است.",
    );
  }

  if (minUnits > 0 && units < minUnits) {
    throw new Error(
      `حداقل تعداد واحد قابل صدور ${minUnits.toLocaleString("fa-IR")} واحد است.`,
    );
  }

  if (maxUnits > 0 && units > maxUnits) {
    throw new Error(
      `حداکثر تعداد واحد مجاز برای این صندوق ${maxUnits.toLocaleString("fa-IR")} واحد است.`,
    );
  }

  if (units > remainedUnits) {
    throw new Error(
      `در حال حاضر حداکثر ${remainedUnits.toLocaleString("fa-IR")} واحد قابل صدور است.`,
    );
  }
};

export const createIssuanceQuote = async ({
  user,
  fundCode,
  investmentAmountRial,
}) => {
  const fund = await getTelegramFund(fundCode, { allowCached: false });
  if (!fund) {
    throw new Error("این صندوق برای خرید از طریق ربات فعال نیست.");
  }

  await ensureUserInvestmentReady({ user, fundCode: resolveFundCode(fund) });

  const grossAmountRial = Number(investmentAmountRial);
  if (!Number.isSafeInteger(grossAmountRial) || grossAmountRial <= 0) {
    throw new Error("مبلغ سرمایه‌گذاری باید یک عدد صحیح و مثبت باشد.");
  }

  const unitPrice = Math.round(parseNumber(fund?.nav?.subscriptionNav));
  if (unitPrice <= 0) {
    throw new Error("NAV صدور/خرید صندوق در دسترس نیست.");
  }

  const units = Math.floor(grossAmountRial / unitPrice);
  const fixedWage = getWageSetting(fund, "IssuanceFixedWage");
  const variableWage = calculateVariableWage(
    grossAmountRial,
    getWageSetting(fund, "IssuanceVariableWageRate"),
    getWageSetting(fund, "IssuanceMaxVariableWage"),
  );
  const wageAmountRial = Math.round(fixedWage + variableWage);
  const amountRial = Math.round(grossAmountRial + wageAmountRial);

  validateIssuanceQuote({ fund, units, amountRial });

  return {
    operation: "issuance",
    fundCode: resolveFundCode(fund),
    fundSeoRegisterNo: resolveFundSeoRegisterNo(fund),
    fundName: String(fund.fundName || "").trim(),
    fundKey: resolveGatewayFundKey(resolveFundCode(fund)),
    unitPrice,
    units,
    grossAmountRial,
    wageAmountRial,
    amountRial,
    inputValue: grossAmountRial,
    createdAt: Date.now(),
    expiresAt: Date.now() + telegramBotConfig.quoteTtlSeconds * 1000,
  };
};

const resolveTadbirRedeemableUnits = (balance) => {
  const remainingUnit = parseNumber(balance?.remainingUnit);
  const remainingWithGuaranteeAndRequest = parseNumber(
    balance?.remainingWithGuaranteeAndRequest,
  );

  if (remainingWithGuaranteeAndRequest > 0) {
    return Math.max(
      0,
      Math.min(remainingUnit, remainingWithGuaranteeAndRequest),
    );
  }

  return Math.max(0, remainingUnit);
};

const resolveSepahRedeemableUnits = (remain) => {
  const adjustedRemain = parseNumber(remain?.AdjustedRemain);
  const currentRemain = parseNumber(remain?.CurrentRemain);
  const blockedRemain = parseNumber(remain?.BlockedRemain);
  const netCurrentRemain = Math.max(0, currentRemain - blockedRemain);

  return adjustedRemain > 0
    ? Math.min(adjustedRemain, netCurrentRemain || adjustedRemain)
    : netCurrentRemain;
};

const getRedeemableUnits = async ({ user, fund }) => {
  const identifier = identityHelpers.getUserIdentifier(user);
  const fundCode = resolveFundCode(fund);

  if (isSepahBrokerFund(fundCode)) {
    const remain = await sepahBrokerServices.getCustomerRemain(identifier);
    return {
      units: resolveSepahRedeemableUnits(remain),
      raw: remain,
    };
  }

  const profile = await tadbirServices.getInvestmentProfile(identifier);
  const fundSeoRegisterNo = resolveFundSeoRegisterNo(fund);
  const balance = (Array.isArray(profile) ? profile : []).find(
    (item) =>
      String(item?.fundSeoRegisterNo || "").trim() === fundSeoRegisterNo,
  );

  return {
    units: balance ? resolveTadbirRedeemableUnits(balance) : 0,
    raw: balance || null,
  };
};

export const getTelegramRedeemableUnits = async ({ user, fundCode }) => {
  const fund = await getTelegramFund(fundCode, { allowCached: false });
  if (!fund) {
    throw new Error("این صندوق برای فروش از طریق ربات فعال نیست.");
  }

  await ensureUserInvestmentReady({ user, fundCode: resolveFundCode(fund) });
  const balance = await getRedeemableUnits({ user, fund });
  return balance.units;
};

export const createRedemptionQuote = async ({ user, fundCode, units }) => {
  const fund = await getTelegramFund(fundCode, { allowCached: false });
  if (!fund) {
    throw new Error("این صندوق برای فروش از طریق ربات فعال نیست.");
  }

  await ensureUserInvestmentReady({ user, fundCode: resolveFundCode(fund) });

  const requestedUnits = Number(units);
  if (!Number.isSafeInteger(requestedUnits) || requestedUnits <= 0) {
    throw new Error("تعداد واحد فروش/ابطال باید یک عدد صحیح و مثبت باشد.");
  }

  const unitPrice = Math.round(parseNumber(fund?.nav?.cancelNav));
  if (unitPrice <= 0) {
    throw new Error("NAV ابطال/فروش صندوق در دسترس نیست.");
  }

  const balance = await getRedeemableUnits({ user, fund });
  if (balance.units <= 0) {
    throw new Error("در حال حاضر واحد قابل ابطال/فروشی ندارید.");
  }

  if (requestedUnits > balance.units) {
    throw new Error(
      `حداکثر تعداد قابل ابطال/فروش ${balance.units.toLocaleString("fa-IR")} واحد است.`,
    );
  }

  const grossAmountRial = Math.round(requestedUnits * unitPrice);
  const wageAmountRial = Math.round(
    getWageSetting(fund, "RedemptionFixedWage"),
  );
  const amountRial = Math.round(grossAmountRial - wageAmountRial);
  if (amountRial <= 0) {
    throw new Error("مبلغ نهایی ابطال/فروش معتبر نیست.");
  }

  return {
    operation: "redemption",
    fundCode: resolveFundCode(fund),
    fundSeoRegisterNo: resolveFundSeoRegisterNo(fund),
    fundName: String(fund.fundName || "").trim(),
    fundKey: resolveGatewayFundKey(resolveFundCode(fund)),
    unitPrice,
    units: requestedUnits,
    redeemableUnits: balance.units,
    grossAmountRial,
    wageAmountRial,
    amountRial,
    inputValue: requestedUnits,
    createdAt: Date.now(),
    expiresAt: Date.now() + telegramBotConfig.quoteTtlSeconds * 1000,
  };
};

export const refreshTelegramQuote = async ({ user, quote }) => {
  if (!quote || quote.expiresAt < Date.now()) {
    const error = new Error("پیش‌نمایش منقضی شده است. دوباره تلاش کنید.");
    error.code = "QUOTE_EXPIRED";
    throw error;
  }

  const refreshed =
    quote.operation === "issuance"
      ? await createIssuanceQuote({
          user,
          fundCode: quote.fundCode,
          investmentAmountRial: quote.inputValue,
        })
      : await createRedemptionQuote({
          user,
          fundCode: quote.fundCode,
          units: quote.inputValue,
        });

  const unchanged = [
    "fundCode",
    "fundSeoRegisterNo",
    "unitPrice",
    "units",
    "wageAmountRial",
    "amountRial",
  ].every((field) => String(refreshed[field]) === String(quote[field]));

  if (!unchanged) {
    const error = new Error(
      "NAV، کارمزد یا ظرفیت صندوق تغییر کرده است. اطلاعات جدید را دوباره تایید کنید.",
    );
    error.code = "QUOTE_CHANGED";
    error.quote = refreshed;
    throw error;
  }

  return refreshed;
};

const ensureUserInvestmentReady = async ({ user, fundCode }) => {
  const userInfo = await models.userInfo
    .findOne({ user: user._id })
    .select("_id")
    .lean();

  if (user.isSejamVerified !== true || !userInfo?._id) {
    const error = new Error(
      "برای انجام این عملیات، اطلاعات سجام شما باید کامل و همگام باشد.",
    );
    error.code = "SEJAM_VERIFICATION_REQUIRED";
    throw error;
  }

  if (isSepahBrokerFund(fundCode)) {
    const sepahStatus =
      await sepahVerificationHelpers.syncUserSepahVerification(user, {
        throwOnLookupFailure: true,
      });
    if (!sepahStatus.isSepahVerified) {
      const error = new Error(
        "برای این صندوق ابتدا باید ثبت‌نام کارگزاری سپه را تکمیل کنید.",
      );
      error.code = "SEPAH_VERIFICATION_REQUIRED";
      error.registerUrl = servicesConfig.sepahBrokerConfig.registerUrl;
      throw error;
    }
    return;
  }

  if (user.isTadbirVerified !== true) {
    await tadbirServices.tadbirCreateUser(user);
    user.isTadbirVerified = true;
  }
};

const quoteResultKey = (quoteId) =>
  `telegram:bot:quote-result:${String(quoteId)}`;

const claimQuote = async (quoteId) => {
  const key = quoteResultKey(quoteId);
  const existing = await redisConnection.get(key);
  if (existing) {
    const parsed = JSON.parse(existing);
    if (parsed.status === "completed") {
      return { alreadyCompleted: true, result: parsed.result };
    }
    throw new Error("این درخواست در حال پردازش است.");
  }

  const claimed = await redisConnection.set(
    key,
    JSON.stringify({ status: "processing" }),
    "EX",
    10 * 60,
    "NX",
  );
  if (claimed !== "OK") {
    throw new Error("این درخواست در حال پردازش است.");
  }

  return { alreadyCompleted: false };
};

const completeQuote = async (quoteId, result) => {
  await redisConnection.setex(
    quoteResultKey(quoteId),
    24 * 60 * 60,
    JSON.stringify({ status: "completed", result }),
  );
};

const releaseQuote = async (quoteId) => {
  await redisConnection.del(quoteResultKey(quoteId));
};

export const startTelegramIssuance = async ({
  user,
  telegramId,
  quoteId,
  quote,
}) => {
  const claim = await claimQuote(quoteId);
  if (claim.alreadyCompleted) {
    return claim.result;
  }

  try {
    await ensureUserInvestmentReady({ user, fundCode: quote.fundCode });
    const currentQuote = await refreshTelegramQuote({ user, quote });
    const identifier = identityHelpers.getUserIdentifier(user);
    const result = await paymentHelpers.startPayment({
      body: {
        confirm: true,
        redirect: false,
        fundId: currentQuote.fundCode,
        fundRegisterCode: currentQuote.fundCode,
        fundSeoRegisterNo: currentQuote.fundSeoRegisterNo,
        fundName: currentQuote.fundName,
        fundKey: currentQuote.fundKey,
        payerType: user.isPrivatePerson === false ? "corporate" : "personal",
        paymentMethod: "gateway",
        amountRial: currentQuote.amountRial,
        units: currentQuote.units,
        unitPrice: currentQuote.unitPrice,
        meta: {
          source: "telegram-bot",
          telegramId: String(telegramId),
          quoteId,
          fundRegisterCode: currentQuote.fundCode,
          grossAmountRial: currentQuote.grossAmountRial,
          wageAmount: currentQuote.wageAmountRial,
        },
      },
      params: { fundId: currentQuote.fundCode },
      user,
      req: {
        identifier,
        ip: "telegram-bot",
        get: (name) =>
          String(name || "").toLowerCase() === "user-agent"
            ? "omidib-telegram-bot"
            : "",
      },
    });

    await completeQuote(quoteId, result);
    return result;
  } catch (error) {
    await releaseQuote(quoteId);
    throw error;
  }
};

const invokeRedemptionController = async ({ user, body }) => {
  let statusCode = 200;
  let responseBody;
  const response = {
    status(value) {
      statusCode = Number(value);
      return this;
    },
    send(value) {
      responseBody = value;
      return this;
    },
  };

  await RequestController.CREATE_REDEMPTION(
    {
      user,
      identifier: identityHelpers.getUserIdentifier(user),
      body,
    },
    response,
  );

  if (statusCode >= 400) {
    const error = new Error(
      responseBody?.message || "ثبت درخواست ابطال/فروش ناموفق بود.",
    );
    error.status = statusCode;
    throw error;
  }

  return responseBody;
};

export const submitTelegramRedemption = async ({
  user,
  telegramId,
  quoteId,
  quote,
}) => {
  const claim = await claimQuote(quoteId);
  if (claim.alreadyCompleted) {
    return claim.result;
  }

  try {
    await ensureUserInvestmentReady({ user, fundCode: quote.fundCode });
    const currentQuote = await refreshTelegramQuote({ user, quote });
    const result = await invokeRedemptionController({
      user,
      body: {
        fundKey: currentQuote.fundKey,
        fundId: currentQuote.fundCode,
        fundSeoRegisterNo: currentQuote.fundSeoRegisterNo,
        fundName: currentQuote.fundName,
        units: currentQuote.units,
        unitPrice: currentQuote.unitPrice,
        amountRial: currentQuote.amountRial,
        meta: {
          source: "telegram-bot",
          telegramId: String(telegramId),
          quoteId,
          grossAmountRial: currentQuote.grossAmountRial,
          wageAmount: currentQuote.wageAmountRial,
          fundRegisterCode: currentQuote.fundCode,
        },
      },
    });

    await completeQuote(quoteId, result);
    return result;
  } catch (error) {
    await releaseQuote(quoteId);
    throw error;
  }
};

export const getTelegramRequestStatus = async ({ user, requestId }) => {
  let request = await models.request.findOne({
    _id: requestId,
    investor: user._id,
  });

  if (!request) {
    throw new Error("درخواست یافت نشد.");
  }

  if (
    request.externalSource === SEPAH_BROKER_EXTERNAL_SOURCE &&
    request.operationType === "issuance" &&
    request.paymentMethod === "gateway" &&
    request.status === "wait_for_payment"
  ) {
    await paymentHelpers.confirmSepahPayment({ requestId, user });
    request = await models.request.findOne({
      _id: requestId,
      investor: user._id,
    });
  }

  return request;
};

export const getLatestTelegramRequests = ({ user, limit = 5 }) =>
  models.request
    .find({ investor: user._id })
    .sort({ createdAt: -1 })
    .limit(Math.max(1, Math.min(Number(limit) || 5, 10)))
    .lean();
