import models from "$models/index.js";
import * as clubHelpers from "$helpers/club/club.helper.js";
import mongoose from "mongoose";
import { identityHelpers } from "$helpers/index.js";

export const EXTERNAL_SYSTEM_TADBIR = "tadbir";
export const REQUEST_HISTORY_EMBED_LIMIT = 10;

const requestFields = [
  "investor",
  "fundSeoRegisterNo",
  "fundName",
  "trackId",
  "transaction",
  "operationType",
  "paymentMethod",
  "paymentCard",
  "paymentSlip",
  "meta",
  "paymentGateway",
  "amountRial",
  "units",
  "unitPrice",
  "status",
  "externalSource",
  "externalId",
  "externalStatus",
  "externalLastResponse",
  "externalLastSyncedAt",
  "createdAt",
  "updatedAt",
];

const relationFields = new Set([
  "investor",
  "transaction",
  "paymentCard",
  "paymentSlip",
]);

const FUND_BANK_ACCOUNT_NUMBERS = {
  11343: "100610810707077376",
  11383: "100610810707077372",
  11141: "100610810707077377",
};

const normalizeString = (value) => String(value || "").trim();
const normalizeStateKey = (value) =>
  normalizeString(value).toLowerCase().replaceAll(/\s+/g, "");

const pickFirstString = (sources = []) => {
  for (const value of sources) {
    const normalizedValue = normalizeString(value);
    if (normalizedValue) {
      return normalizedValue;
    }
  }

  return "";
};

export const buildErrorSnapshot = (error) => ({
  error: true,
  message:
    error?.response?.data?.errorMessage ||
    error?.response?.data?.message ||
    error?.message ||
    "External request failed.",
  status: error?.response?.status || null,
  data: error?.response?.data || null,
  at: new Date().toISOString(),
});

const populateRequestForExternalOps = (query) =>
  query
    .populate("investor", identityHelpers.USER_IDENTITY_PROJECTION)
    .populate("transaction")
    .populate({
      path: "paymentSlip",
      populate: {
        path: "receiptFile",
      },
    });

export const ensureLoadedRequest = async (requestOrId) => {
  const requestId = requestOrId?._id || requestOrId;
  if (!requestId) {
    return null;
  }

  if (
    requestOrId?.investor &&
    requestOrId?.operationType &&
    typeof requestOrId.save === "function"
  ) {
    return requestOrId;
  }

  return populateRequestForExternalOps(models.request.findById(requestId));
};

const isObjectIdLike = (value) =>
  value instanceof mongoose.Types.ObjectId ||
  (value &&
    typeof value === "object" &&
    value?._bsontype === "ObjectID" &&
    typeof value?.toString === "function");

const sanitizeHistoryValue = (value) => {
  if (value === undefined) {
    return undefined;
  }

  if (value === null) {
    return null;
  }

  if (value instanceof Date) {
    return value.toISOString();
  }

  if (isObjectIdLike(value)) {
    return value.toString();
  }

  if (Array.isArray(value)) {
    return value.map((item) => sanitizeHistoryValue(item));
  }

  if (typeof value === "object") {
    const plainObject =
      typeof value?.toObject === "function" ? value.toObject() : value;
    return Object.entries(plainObject).reduce((result, [key, currentValue]) => {
      const sanitizedValue = sanitizeHistoryValue(currentValue);
      if (sanitizedValue !== undefined) {
        result[key] = sanitizedValue;
      }
      return result;
    }, {});
  }

  return value;
};

const buildComparableString = (value) =>
  JSON.stringify(sanitizeHistoryValue(value));

const buildRequestSnapshot = (request) => {
  if (!request) {
    return {};
  }

  const source =
    typeof request?.toObject === "function" ? request.toObject() : request;

  return requestFields.reduce((snapshot, field) => {
    const rawValue =
      relationFields.has(field) && source[field]?._id
        ? source[field]._id
        : source[field];
    const sanitizedValue = sanitizeHistoryValue(rawValue);
    if (sanitizedValue !== undefined) {
      snapshot[field] = sanitizedValue;
    }
    return snapshot;
  }, {});
};

const buildChangedFields = (beforeSnapshot = {}, afterSnapshot = {}) => {
  const keys = new Set([
    ...Object.keys(beforeSnapshot || {}),
    ...Object.keys(afterSnapshot || {}),
  ]);

  return Array.from(keys).reduce((changes, key) => {
    const previousValue = beforeSnapshot?.[key];
    const nextValue = afterSnapshot?.[key];

    if (
      buildComparableString(previousValue) !== buildComparableString(nextValue)
    ) {
      changes[key] = {
        from: previousValue === undefined ? null : previousValue,
        to: nextValue === undefined ? null : nextValue,
      };
    }

    return changes;
  }, {});
};

const resolveActorPayload = ({ actorUser, actorType } = {}) => {
  const resolvedActorUser = actorUser?._id || actorUser || null;
  return {
    actorType: resolvedActorUser ? "user" : actorType || "system",
    actorUser: resolvedActorUser || null,
  };
};

const resolveEventType = ({ eventType, changedFields, nextSnapshot } = {}) => {
  if (eventType) {
    return eventType;
  }

  if (changedFields?.status) {
    return "status_changed";
  }

  if (changedFields?.paymentSlip && nextSnapshot?.paymentSlip) {
    return "payment_slip_attached";
  }

  if (changedFields?.transaction && nextSnapshot?.transaction) {
    return "transaction_linked";
  }

  return "updated";
};

const createHistoryEntry = async (requestId, payload = {}) => {
  await models.requestHistory.create({
    request: requestId,
    ...payload,
  });
};

export const createRequestWithHistory = async (
  requestData = {},
  {
    actorUser,
    actorType,
    source = "request_controller",
    eventType = "created",
    notes = "",
  } = {},
) => {
  const createdRequest = await models.request.create(requestData);
  const snapshot = buildRequestSnapshot(createdRequest);

  await createHistoryEntry(createdRequest._id, {
    ...resolveActorPayload({ actorUser, actorType }),
    source,
    eventType,
    changedFields: Object.entries(snapshot).reduce((changes, [key, value]) => {
      changes[key] = { from: null, to: value };
      return changes;
    }, {}),
    snapshot,
    notes,
  });

  return createdRequest;
};

export const updateRequestWithHistory = async (
  requestOrId,
  updateData = {},
  {
    actorUser,
    actorType,
    source = "request_controller",
    eventType = "",
    notes = "",
  } = {},
) => {
  const request = await ensureLoadedRequest(requestOrId);
  if (!request) {
    return null;
  }

  const beforeSnapshot = buildRequestSnapshot(request);
  Object.assign(request, updateData);
  const pendingSnapshot = buildRequestSnapshot(request);
  const pendingChanges = buildChangedFields(beforeSnapshot, pendingSnapshot);

  if (Object.keys(pendingChanges).length === 0) {
    return request;
  }

  await request.save();
  const afterSnapshot = buildRequestSnapshot(request);
  const changedFields = buildChangedFields(beforeSnapshot, afterSnapshot);

  await createHistoryEntry(request._id, {
    ...resolveActorPayload({ actorUser, actorType }),
    source,
    eventType: resolveEventType({
      eventType,
      changedFields,
      nextSnapshot: afterSnapshot,
    }),
    changedFields,
    snapshot: afterSnapshot,
    notes,
  });

  if (
    beforeSnapshot?.status !== "approved" &&
    afterSnapshot?.status === "approved" &&
    afterSnapshot?.operationType === "issuance"
  ) {
    try {
      await clubHelpers.awardFirstInvestmentPointsForRequest(request);
    } catch (error) {
      console.error("[club] failed to process approved issuance reward", {
        requestId: request._id,
        error: error?.message || error,
      });
    }
  }

  return request;
};

export const deleteRequestWithHistory = async (
  requestOrId,
  {
    actorUser,
    actorType,
    source = "request_controller",
    eventType = "deleted",
    notes = "",
  } = {},
) => {
  const request = await ensureLoadedRequest(requestOrId);
  if (!request) {
    return null;
  }

  const snapshot = buildRequestSnapshot(request);

  await createHistoryEntry(request._id, {
    ...resolveActorPayload({ actorUser, actorType }),
    source,
    eventType,
    changedFields: {},
    snapshot,
    notes,
  });

  await models.request.deleteOne({ _id: request._id });
  return request;
};

const populateRequestHistory = (query) =>
  query.populate("actorUser", identityHelpers.USER_IDENTITY_PROJECTION);

export const getRequestHistory = async (
  requestId,
  { page = 1, pageSize = 20, sort = "asc" } = {},
) => {
  const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
  const normalizedPage = Math.max(1, Number(page) || 1);
  const skip = normalizedPageSize * (normalizedPage - 1);
  const sortDirection = String(sort).toLowerCase() === "desc" ? -1 : 1;
  const query = { request: requestId };

  const [history, totalData] = await Promise.all([
    populateRequestHistory(
      models.requestHistory
        .find(query)
        .sort({ createdAt: sortDirection, _id: sortDirection })
        .skip(skip)
        .limit(normalizedPageSize),
    ),
    models.requestHistory.countDocuments(query),
  ]);

  return {
    history,
    totalData,
    currentPage: normalizedPage,
    totalPage: Math.ceil(totalData / normalizedPageSize) || 0,
  };
};

export const getEmbeddedRequestHistory = async (
  requestId,
  { limit = REQUEST_HISTORY_EMBED_LIMIT } = {},
) => {
  const normalizedLimit = Math.max(
    1,
    Number(limit) || REQUEST_HISTORY_EMBED_LIMIT,
  );
  const query = { request: requestId };
  const [history, totalData] = await Promise.all([
    populateRequestHistory(
      models.requestHistory
        .find(query)
        .sort({ createdAt: -1, _id: -1 })
        .limit(normalizedLimit),
    ),
    models.requestHistory.countDocuments(query),
  ]);

  return {
    history,
    historyCount: totalData,
  };
};

export const mapExternalStateToLocalStatus = (stateKey) => {
  const normalizedStateKey = normalizeStateKey(stateKey);

  if (!normalizedStateKey) {
    return "";
  }

  if (normalizedStateKey === "wait") {
    return "reviewing";
  }

  if (normalizedStateKey === "accept") {
    return "approved";
  }

  if (normalizedStateKey === "fail") {
    return "rejected";
  }

  if (normalizedStateKey === "deleted") {
    return "failed";
  }

  return "";
};

const toOptionalBoolean = (value) => {
  if (typeof value === "boolean") {
    return value;
  }

  if (value === "true") return true;
  if (value === "false") return false;
  return undefined;
};

const toOptionalInteger = (value) => {
  const numericValue = Number(value);
  return Number.isInteger(numericValue) ? numericValue : undefined;
};

const buildReceiptFilePayload = (file) => {
  if (!file?.content) {
    return {
      receiptFile: undefined,
      fileName: "",
    };
  }

  return {
    receiptFile: Buffer.from(file.content).toString("base64"),
    fileName: pickFirstString([
      file?.name,
      file?.extension ? `${file._id}.${file.extension}` : "",
      String(file?._id || ""),
    ]),
  };
};

export const buildIssuancePayload = (request) => {
  const meta = request?.meta || {};
  const tadbirMeta = meta?.tadbir || meta;
  const transaction = request?.transaction || {};
  const paymentSlip = request?.paymentSlip || {};
  const investorIdentifier = identityHelpers.getUserIdentifier(
    request?.investor || {},
  );
  const paymentMethod = normalizeString(request?.paymentMethod).toLowerCase();
  const paymentMethodIsGateway = paymentMethod === "gateway";
  const paymentMethodIsCard = paymentMethod === "card";
  const paymentMethodIsSlip = paymentMethod === "slip";
  const bankBillCode = pickFirstString([
    tadbirMeta?.BankBillCode,
    tadbirMeta?.bankBillCode,
    paymentSlip?.bankBillCode,
    transaction?.rrn,
    request?.paymentGateway?.rrn,
    transaction?.orderId,
    request?.paymentGateway?.orderId,
    request?.trackId,
  ]);

  const payload = {
    InvestorNationalCode: normalizeString(investorIdentifier),
    Amount: Math.round(Number(request?.amountRial || 0)),
    BankBillCode: bankBillCode,
    FundBankAccountNumber:
      FUND_BANK_ACCOUNT_NUMBERS[
        String(request?.fundSeoRegisterNo || "").trim()
      ],
    IsInstantPayment: paymentMethodIsGateway,
  };

  if (paymentMethodIsGateway) {
    payload.PaymentMethod = 1;
  }

  if (paymentMethodIsCard) {
    payload.PaymentMethod = 7;
  }

  if (paymentMethodIsSlip) {
    const receiptFilePayload = buildReceiptFilePayload(
      paymentSlip?.receiptFile,
    );

    if (receiptFilePayload.receiptFile) {
      payload.receiptFile = receiptFilePayload.receiptFile;
    }

    if (receiptFilePayload.fileName) {
      payload.BankBillImageFileName = receiptFilePayload.fileName;
    }
  }

  return payload;
};

export const buildRedemptionPayload = (request) => {
  const meta = request?.meta || {};
  const tadbirMeta = meta?.tadbir || meta;
  const count = Math.round(Number(request?.units ?? 0));

  const payload = {
    nationalCode: normalizeString(request?.investor?.nationalId),
    count,
  };

  const reasonId = toOptionalInteger(
    tadbirMeta?.ReasonId ?? tadbirMeta?.reasonId,
  );
  if (reasonId !== undefined) {
    payload.ReasonId = reasonId;
  }

  const reason = pickFirstString([tadbirMeta?.Reason, tadbirMeta?.reason]);
  if (reason) {
    payload.Reason = reason;
  }

  const settlementPercent = Number(
    tadbirMeta?.RedemptionRequestSettlementToBrokerPercent ??
      tadbirMeta?.redemptionRequestSettlementToBrokerPercent,
  );
  if (Number.isFinite(settlementPercent)) {
    payload.RedemptionRequestSettlementToBrokerPercent = settlementPercent;
  }

  return payload;
};

export const persistRequestUpdate = async (
  request,
  updateData = {},
  historyOptions = {},
) => {
  return updateRequestWithHistory(request, updateData, {
    actorType: "system",
    source: EXTERNAL_SYSTEM_TADBIR,
    ...historyOptions,
  });
};
