import crypto from "crypto";
import jwt from "jsonwebtoken";

import { appConfig } from "$config/index.js";
import { redisConnection } from "$connections/index.js";
import * as identityHelpers from "$helpers/identity/identity.helper.js";
import * as testModeHelpers from "$helpers/test-mode/test-mode.helper.js";
import models from "$models/index.js";

export const OTP_TTL_SECONDS = 2 * 60;
export const CAPTCHA_TTL_SECONDS = 5 * 60;
export const CAPTCHA_MAX_ATTEMPTS = 3;
export const IMPERSONATION_LINK_TTL_SECONDS = 30;
export const DEV_USERS_OTP = "12345";

const DEV_USERS_IDS = ["0019431171", "0019277792", "3240309653"];
const TEST_SEJAM_BANK_ID = "130";

export const isDevelopmentUsers = (user) =>
  !appConfig.published &&
  DEV_USERS_IDS.includes(identityHelpers.getUserIdentifier(user));

export const normalizeIpAddress = (value = "") => {
  if (!value) {
    return null;
  }

  const firstValue = String(value).split(",")[0].trim();

  if (!firstValue) {
    return null;
  }

  if (firstValue === "::1") {
    return "127.0.0.1";
  }

  if (firstValue.startsWith("::ffff:")) {
    return firstValue.replace("::ffff:", "");
  }

  return firstValue;
};

export const getClientIpAddress = (req) =>
  normalizeIpAddress(
    req.headers["x-forwarded-for"] ||
      req.headers["x-real-ip"] ||
      req.ip ||
      req.socket?.remoteAddress,
  );

export const signAuthToken = (user) =>
  jwt.sign(
    {
      id: user._id,
      identifier: identityHelpers.getUserIdentifier(user),
    },
    appConfig.secret,
    { expiresIn: appConfig.jwtExpiration },
  );

export const signImpersonationLinkToken = ({ actor, targetUser }) =>
  jwt.sign(
    {
      scope: "impersonation-link",
      actorId: actor._id,
      targetUserId: targetUser._id,
    },
    appConfig.secret,
    { expiresIn: IMPERSONATION_LINK_TTL_SECONDS },
  );

export const ensureTestModeAccess = (identifier) => {
  if (!appConfig.testMode) {
    return;
  }

  if (!testModeHelpers.isTestNationalCodeAllowed(identifier)) {
    throw {
      status: 403,
      message: "دسترسی این کد ملی در حالت تست مجاز نیست",
    };
  }
};

export const shouldUseMockSejamFlow = ({
  identifier,
  userExists = false,
} = {}) =>
  !appConfig.published &&
  appConfig.testMode &&
  !userExists &&
  testModeHelpers.isTestNationalCodeAllowed(identifier);

export const buildMockSejamProfile = ({ identifier, isPrivatePerson }) => {
  const normalizedIdentifier = String(identifier || "").trim();
  const identifierSuffix = normalizedIdentifier.slice(-4) || "0000";
  const mobile = `0912${normalizedIdentifier.slice(-7).padStart(7, "0")}`;

  return {
    data: {
      type: isPrivatePerson ? "IranianPrivatePerson" : "IranianLegalPerson",
      mobile,
      email: isPrivatePerson
        ? `test-${normalizedIdentifier}@omidib.local`
        : `legal-${normalizedIdentifier}@omidib.local`,
      privatePerson: isPrivatePerson
        ? {
            firstName: "تستی",
            lastName: `کاربر ${identifierSuffix}`,
            fatherName: "تست",
            gender: "Male",
            birthDate: "1990-01-01T00:00:00.000Z",
            placeOfBirth: "تهران",
            placeOfIssue: "تهران",
            seriShChar: "الف",
            seriSh: "11",
            serial: identifierSuffix,
            shNumber: `SH${identifierSuffix}`,
          }
        : {},
      legalPerson: isPrivatePerson
        ? {}
        : {
            companyName: `شرکت تست ${identifierSuffix}`,
            name: `شرکت تست ${identifierSuffix}`,
            title: `شرکت تست ${identifierSuffix}`,
            fullName: `شرکت تست ${identifierSuffix}`,
          },
      addresses: [
        {
          province: { name: "تهران" },
          city: { name: "تهران" },
          postalCode: `19999${identifierSuffix}`.slice(0, 10),
          remnantAddress: "آدرس تستی برای احراز هویت",
          alley: "کوچه تست",
          plaque: "1",
          email: isPrivatePerson
            ? `test-${normalizedIdentifier}@omidib.local`
            : `legal-${normalizedIdentifier}@omidib.local`,
        },
      ],
      accounts: [
        {
          sheba: `IR82054010268002081790900${identifierSuffix}`.slice(0, 26),
          accountNumber: `1000${identifierSuffix}`,
          isDefault: true,
          modifiedDate: new Date().toISOString(),
          bank: {
            id: TEST_SEJAM_BANK_ID,
            name: "بانک ملت",
          },
        },
      ],
    },
  };
};

export const getImpersonationLinkKey = (token) =>
  `auth:impersonation-link:${crypto.createHash("sha256").update(token).digest("hex")}`;

export const consumeImpersonationLinkKey = async (token) => {
  const linkKey = getImpersonationLinkKey(token);
  const result = await redisConnection.eval(
    `
      local value = redis.call("GET", KEYS[1])
      if value then
        redis.call("DEL", KEYS[1])
      end
      return value
    `,
    1,
    linkKey,
  );

  return typeof result === "string" ? result : null;
};

export const buildUserWithPermissionDetails = async (user) => {
  if (!user) {
    return null;
  }

  const userObject = user.toObject();
  const permissionKeys = userObject.role?.permissions || [];

  if (!userObject.role || permissionKeys.length === 0) {
    return userObject;
  }

  const permissionDocs = await models.permission
    .find({
      key: { $in: permissionKeys },
    })
    .lean();

  const permissionMap = new Map(permissionDocs.map((item) => [item.key, item]));

  userObject.role.permissions = permissionKeys
    .map((key) => permissionMap.get(key))
    .filter(Boolean);

  return userObject;
};

export const saveSejamUserAndInfo = async (
  identifier,
  sejamPayload,
  options = {},
) => {
  const sejamProfile = sejamPayload?.data || {};
  const isPrivatePerson = identityHelpers.resolveIsPrivatePerson(
    sejamProfile.type,
  );
  const privatePerson = sejamProfile.privatePerson || {};
  const legalPerson = sejamProfile.legalPerson || {};
  const address = sejamProfile.addresses?.[0] || {};
  const bankAccount = sejamProfile.accounts?.[0] || {};
  const tradingCodes = sejamProfile?.tradingCodes || [];
  const sejamBankId = String(bankAccount.bank?.id || "").trim();
  const resolvedBank = sejamBankId
    ? await models.bank.findOne({ sejamBankId })
    : null;
  const mobile = sejamProfile.mobile?.toString() || "";
  const fullName = isPrivatePerson
    ? [privatePerson.firstName, privatePerson.lastName]
        .filter(Boolean)
        .join(" ")
    : String(
        legalPerson.companyName ||
          legalPerson.name ||
          legalPerson.title ||
          legalPerson.fullName ||
          "",
      ).trim();

  const existingUser = await models.user.findOne(
    isPrivatePerson ? { nationalId: identifier } : { businessID: identifier },
  );
  const defaultRole = await models.role.findOne({ value: "user" });
  const role =
    options.preserveExistingRole && existingUser?.role
      ? existingUser.role
      : defaultRole;
  const identifierFields = isPrivatePerson
    ? { nationalId: identifier }
    : { businessID: identifier };
  const oppositeIdentifierField = isPrivatePerson ? "businessID" : "nationalId";

  const user = await models.user
    .findOneAndUpdate(
      identifierFields,
      {
        $set: {
          ...identifierFields,
          fullName,
          mobile,
          isPrivatePerson,
          isSejamVerified: true,
          isTadbirVerified: false,
          role,
        },
        $unset: {
          [oppositeIdentifierField]: 1,
        },
      },
      { new: true, upsert: true },
    )
    .populate("role");

  await models.sejam.findOneAndUpdate(
    { user: user._id },
    {
      $set: {
        user: user._id,
        ...identifierFields,
        isPrivatePerson,
        data:
          sejamPayload?.data &&
          typeof sejamPayload.data === "object" &&
          !Array.isArray(sejamPayload.data)
            ? sejamPayload.data
            : {},
      },
      $unset: {
        [oppositeIdentifierField]: 1,
      },
    },
    { new: true, upsert: true },
  );

  await models.userInfo.findOneAndUpdate(
    { user: user._id },
    {
      $set: {
        user: user._id,
        firstName: privatePerson.firstName,
        lastName: privatePerson.lastName,
        fatherName: privatePerson.fatherName,
        gender: privatePerson.gender,
        birthDate: privatePerson.birthDate
          ? new Date(privatePerson.birthDate)
          : null,
        placeOfBirth: privatePerson.placeOfBirth,
        placeOfIssue: privatePerson.placeOfIssue,
        seriShChar: privatePerson.seriShChar,
        seriSh: privatePerson.seriSh,
        serial: privatePerson.serial,
        shNumber: privatePerson.shNumber,
        mobile,
        tradingCodes,
        email: sejamProfile.email || address.email || "",
        address: {
          province: address.province?.name || "",
          city: address.city?.name || "",
          postalCode: address.postalCode || "",
          fullAddress: buildFullAddress(address),
          remnantAddress: address.remnantAddress || "",
          alley: address.alley || "",
          plaque: address.plaque || "",
        },
        bankAccount: bankAccount.sheba
          ? {
              sheba: bankAccount.sheba,
              accountNumber: bankAccount.accountNumber,
              bankName: resolvedBank?.name || bankAccount.bank?.name || "",
              bankId: resolvedBank
                ? Number(resolvedBank.sejamBankId)
                : bankAccount.bank?.id,
              isDefault: bankAccount.isDefault,
              lastModified: bankAccount.modifiedDate
                ? new Date(bankAccount.modifiedDate)
                : new Date(),
            }
          : {
              sheba: "",
              accountNumber: "",
              bankName: "",
              bankId: null,
              isDefault: false,
              lastModified: null,
            },
      },
    },
    { new: true, upsert: true },
  );

  return user;
};

export const generateOtp = () => crypto.randomInt(10000, 100000).toString();

export const generateCaptchaId = () => crypto.randomUUID();

export const getOtpKey = (identifier) => `auth:otp:${identifier}`;
export const getOtpResendKey = (identifier) => `auth:otp:resend:${identifier}`;
export const getCaptchaKey = (captchaId) => `auth:captcha:${captchaId}`;

export const grantOtpResendAccess = async (identifier) => {
  await redisConnection.setex(
    getOtpResendKey(identifier),
    OTP_TTL_SECONDS,
    "1",
  );
};

export const validateCaptcha = async (captchaId, captchaAnswer) => {
  if (!captchaId || captchaAnswer === undefined || captchaAnswer === null) {
    throw { status: 400, message: "کپچا الزامی است" };
  }

  const normalizedAnswer = String(captchaAnswer).trim().toLowerCase();

  if (!normalizedAnswer) {
    throw { status: 400, message: "پاسخ کپچا الزامی است" };
  }

  const captchaPayload = await redisConnection.get(getCaptchaKey(captchaId));

  if (!captchaPayload) {
    throw { status: 400, message: "کپچا منقضی شده است، دوباره تلاش کنید" };
  }

  const captcha = JSON.parse(captchaPayload);
  const nextAttempts = Number(captcha.attempts || 0) + 1;

  if (captcha.answer !== normalizedAnswer) {
    if (nextAttempts >= CAPTCHA_MAX_ATTEMPTS) {
      await redisConnection.del(getCaptchaKey(captchaId));
    } else {
      await redisConnection.setex(
        getCaptchaKey(captchaId),
        CAPTCHA_TTL_SECONDS,
        JSON.stringify({
          ...captcha,
          attempts: nextAttempts,
        }),
      );
    }

    throw { status: 400, message: "پاسخ کپچا نامعتبر است" };
  }

  await redisConnection.del(getCaptchaKey(captchaId));
};

export const validateOtpResendAccess = async (identifier) => {
  const hasResendAccess = await redisConnection.get(
    getOtpResendKey(identifier),
  );

  if (!hasResendAccess) {
    throw {
      status: 400,
      message: "برای دریافت کد تایید، ابتدا کپچا را در مرحله اول تکمیل کنید",
    };
  }
};

export const validateUserOtp = async (identifier, otp) => {
  const savedOtp = await redisConnection.get(getOtpKey(identifier));

  if (!savedOtp) {
    throw { status: 400, message: "کد تایید منقضی شده است" };
  }

  if (savedOtp !== otp) {
    throw { status: 401, message: "کد تایید نامعتبر است" };
  }
};

export const buildFullAddress = (address) => {
  const parts = [
    address.province?.name,
    address.city?.name,
    address.remnantAddress,
    address.alley,
    address.plaque ? `پلاک ${address.plaque}` : "",
  ].filter(Boolean);

  return parts.join(" - ");
};
