import models from "$models/index.js";
import mongoose from "mongoose";
import { parse } from "node-html-parser";
import { identityHelpers } from "$helpers/index.js";
import { MANUAL_NEWS_AUTHORS } from "$models/news/news.model.js";
import { buildNewsTableQuery, buildPublicNewsQuery } from "./news.query.js";

const IMMUTABLE_FIELDS = ["_id", "createdAt", "updatedAt", "__v", "viewCount"];
const NUMERIC_SLUG_LENGTH = 6;

const normalizeSlug = (value = "") => value.toString().trim();
const isValidNumericSlug = (value = "") =>
  new RegExp(`^\\d{${NUMERIC_SLUG_LENGTH}}$`).test(normalizeSlug(value));

const decodeHtmlText = (value = "") => {
  if (!value) return "";

  const decoded = parse(`<span>${value}</span>`).text;

  return decoded
    .replace(/&nbsp;|&#160;|&#xa0;/gi, " ")
    .replace(/\u00a0/g, " ");
};

const sanitizePlainText = (value = "") =>
  decodeHtmlText(value)
    .replace(/\s+/g, " ")
    .trim();

const normalizeObjectIdValue = (value) => {
  if (!value) return null;

  const candidate =
    typeof value === "object" && value !== null && value._id ? value._id : value;

  return mongoose.Types.ObjectId.isValid(candidate) ? candidate.toString() : null;
};

const normalizeAuthorMode = (value) =>
  value === "manual" ? "manual" : "currentUser";

const normalizeManualAuthorName = (value = "") => sanitizePlainText(value);

const resolveAuthorPayload = ({ body = {}, user }) => {
  const authorMode = normalizeAuthorMode(body.authorMode);

  if (authorMode === "manual") {
    const authorName = normalizeManualAuthorName(body.authorName);

    if (!authorName) {
      const error = new Error("انتخاب نام نویسنده الزامی است.");
      error.status = 422;
      throw error;
    }

    if (!MANUAL_NEWS_AUTHORS.includes(authorName)) {
      const error = new Error("نام نویسنده انتخاب شده معتبر نیست.");
      error.status = 422;
      throw error;
    }

    return {
      author: null,
      authorName,
    };
  }

  return {
    author: user?._id || null,
    authorName: "",
  };
};

const resolveUpdatedAuthorPayload = ({ body = {}, user, existingItem }) => {
  const authorMode = normalizeAuthorMode(body.authorMode);

  if (authorMode === "manual") {
    return resolveAuthorPayload({ body, user });
  }

  return {
    author: existingItem?.author || user?._id || null,
    authorName: "",
  };
};

const sanitizeData = (data = {}) => {
  const sanitized = { ...data };

  IMMUTABLE_FIELDS.forEach((field) => delete sanitized[field]);
  delete sanitized.slug;

  if (sanitized.category) {
    sanitized.category = sanitized.category.trim().toLowerCase();
  }

  ["title", "excerpt", "seoTitle", "seoDescription", "source"].forEach(
    (field) => {
      if (typeof sanitized[field] === "string") {
        sanitized[field] = sanitizePlainText(sanitized[field]);
      }
    },
  );

  if (typeof sanitized.tags === "string") {
    sanitized.tags = sanitized.tags
      .split(",")
      .map((tag) => sanitizePlainText(tag))
      .filter(Boolean);
  }

  if (sanitized.coverImage === "") {
    sanitized.coverImage = null;
  } else if (
    sanitized.coverImage &&
    typeof sanitized.coverImage === "object" &&
    sanitized.coverImage._id
  ) {
    sanitized.coverImage = sanitized.coverImage._id;
  }

  delete sanitized.author;
  delete sanitized.authorMode;
  delete sanitized.authorName;
  delete sanitized.source;
  delete sanitized.status;
  delete sanitized.publishedAt;
  delete sanitized.priority;

  return sanitized;
};

const populateNewsQuery = (query) =>
  query
    .populate("author", identityHelpers.USER_IDENTITY_PROJECTION)
    .populate("coverImage", "name type extension createdAt");

const ensureValidCoverImage = async (coverImageId) => {
  if (!coverImageId) {
    return null;
  }

  if (!mongoose.Types.ObjectId.isValid(coverImageId)) {
    const error = new Error("تصویر شاخص معتبر نیست.");
    error.status = 422;
    throw error;
  }

  const file = await models.file.findById(coverImageId).select("_id");
  if (!file) {
    const error = new Error("فایل تصویر شاخص یافت نشد.");
    error.status = 404;
    throw error;
  }

  return file;
};

const attachCoverImageNameToSlug = async (slug, coverImageId) => {
  const normalizedCoverImageId = normalizeObjectIdValue(coverImageId);
  if (!slug || !normalizedCoverImageId) {
    return null;
  }

  return models.file.findByIdAndUpdate(
    normalizedCoverImageId,
    { $set: { name: slug } },
    { new: true, runValidators: true },
  );
};

const deleteRelatedCoverImage = async ({ slug, coverImageId }) => {
  const normalizedCoverImageId = normalizeObjectIdValue(coverImageId);

  if (normalizedCoverImageId) {
    await models.file.findByIdAndDelete(normalizedCoverImageId);
    return;
  }

  if (slug) {
    await models.file.findOneAndDelete({ name: slug });
  }
};

const createNumericSlugCandidate = () =>
  `${Math.floor(10 ** (NUMERIC_SLUG_LENGTH - 1) + Math.random() * 9 * 10 ** (NUMERIC_SLUG_LENGTH - 1))}`;

const ensureUniqueNumericSlug = async (ignoredId = null) => {
  let attempts = 0;

  while (attempts < 50) {
    const nextSlug = createNumericSlugCandidate();
    const query = { slug: nextSlug };
    if (ignoredId) query._id = { $ne: ignoredId };

    if (!(await models.news.exists(query))) {
      return nextSlug;
    }

    attempts += 1;
  }

  const error = new Error("Could not generate a unique news slug");
  error.status = 500;
  throw error;
};

const ensurePersistedNumericSlug = async (item) => {
  if (!item || isValidNumericSlug(item.slug)) {
    return item;
  }

  item.slug = await ensureUniqueNumericSlug(item._id);
  await item.save();
  return item;
};

const ensurePersistedNumericSlugs = async (items = []) =>
  Promise.all(items.map((item) => ensurePersistedNumericSlug(item)));

export const ALL = async (req, res) => {
  try {
    const {
      pageSize,
      currentPage,
      skip,
      filterQuery,
      sort,
      sortField,
      sortDirection,
    } = buildNewsTableQuery(req.query);

    const news = await populateNewsQuery(models.news.find(filterQuery))
      .limit(pageSize)
      .skip(skip)
      .sort(sort);
    const normalizedNews = await ensurePersistedNumericSlugs(news);

    const totalData = await models.news.countDocuments(filterQuery);
    const totalPage = pageSize ? Math.ceil(totalData / pageSize) : 0;

    res.status(200).send({
      news: normalizedNews,
      totalData,
      currentPage,
      totalPage,
      sortField,
      sortDirection,
    });
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در دریافت اخبار";
    return res.status(status).send({ message });
  }
};

export const PUBLIC_ALL = async (req, res) => {
  try {
    const pageSize = req.query.pageSize ? parseInt(req.query.pageSize) : 20;
    const currentPage = req.query.page ? parseInt(req.query.page) : 1;
    const skip = pageSize * currentPage - pageSize;
    const query = {
      ...buildPublicNewsQuery(req.query),
      isActive: true,
    };

    const news = await populateNewsQuery(
      models.news.find(query),
    )
      .limit(pageSize)
      .skip(skip)
      .sort({ createdAt: -1 });
    const normalizedNews = await ensurePersistedNumericSlugs(news);

    const totalData = await models.news.countDocuments(query);
    const totalPage = pageSize ? Math.ceil(totalData / pageSize) : 0;

    res.status(200).send({
      news: normalizedNews,
      totalData,
      currentPage,
      totalPage,
    });
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در دریافت اخبار";
    return res.status(status).send({ message });
  }
};

export const SINGLE = async (req, res) => {
  try {
    const { id } = req.params;

    const item = await populateNewsQuery(models.news.findById(id));

    if (!item) {
      return res.status(404).send({ message: "خبر یافت نشد" });
    }

    const normalizedItem = await ensurePersistedNumericSlug(item);

    res.status(200).send({ news: normalizedItem });
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در دریافت خبر";
    return res.status(status).send({ message });
  }
};

export const PUBLIC_SINGLE = async (req, res) => {
  try {
    const { slug } = req.params;

    const item = await populateNewsQuery(
      models.news.findOne({
        slug: normalizeSlug(slug),
        isActive: true,
      }),
    );

    if (!item) {
      return res.status(404).send({ message: "خبر یافت نشد" });
    }

    const normalizedItem = await ensurePersistedNumericSlug(item);

    await models.news.updateOne(
      { _id: normalizedItem._id },
      { $inc: { viewCount: 1 } },
    );

    res.status(200).send({
      news: {
        ...normalizedItem.toObject(),
        viewCount: normalizedItem.viewCount + 1,
      },
    });
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در دریافت خبر";
    return res.status(status).send({ message });
  }
};

export const CREATE = async (req, res) => {
  try {
    const data = sanitizeData(req.body);
    await ensureValidCoverImage(data.coverImage);

    data.slug = await ensureUniqueNumericSlug();
    Object.assign(data, resolveAuthorPayload({ body: req.body, user: req.user }));

    const news = await models.news.create(data);
    await attachCoverImageNameToSlug(news.slug, news.coverImage);

    const populatedNews = await populateNewsQuery(models.news.findById(news._id));

    res.status(201).send({
      message: "خبر با موفقیت ایجاد شد",
      news: populatedNews || news,
    });
  } catch (error) {
    const status = error.name === "ValidationError" ? 400 : error.status || 500;
    const message = error.message || "خطا در ایجاد خبر";
    return res.status(status).send({ message });
  }
};

export const UPDATE = async (req, res) => {
  try {
    const { id } = req.params;
    const updateData = sanitizeData(req.body);
    await ensureValidCoverImage(updateData.coverImage);

    const existingItem = await models.news.findById(id);
    if (!existingItem) {
      return res.status(404).send({ message: "خبر یافت نشد" });
    }

    if (updateData.title && !isValidNumericSlug(existingItem.slug)) {
      updateData.slug = await ensureUniqueNumericSlug(id);
    }

    Object.assign(
      updateData,
      resolveUpdatedAuthorPayload({
        body: req.body,
        user: req.user,
        existingItem,
      }),
    );

    const previousCoverImageId = normalizeObjectIdValue(existingItem.coverImage);
    const nextCoverImageId =
      updateData.coverImage === undefined
        ? previousCoverImageId
        : normalizeObjectIdValue(updateData.coverImage);

    const news = await models.news.findByIdAndUpdate(
      id,
      { $set: updateData },
      { new: true, runValidators: true },
    );

    if (updateData.coverImage !== undefined && previousCoverImageId !== nextCoverImageId) {
      await deleteRelatedCoverImage({
        slug: existingItem.slug,
        coverImageId: previousCoverImageId,
      });
    }

    await attachCoverImageNameToSlug(news.slug, news.coverImage);
    const populatedNews = await populateNewsQuery(models.news.findById(news._id));

    res.status(200).send({
      message: "خبر با موفقیت ویرایش شد",
      news: populatedNews || news,
    });
  } catch (error) {
    const status = error.name === "ValidationError" ? 400 : error.status || 500;
    const message = error.message || "خطا در ویرایش خبر";
    return res.status(status).send({ message });
  }
};

export const DELETE = async (req, res) => {
  try {
    const { id } = req.params;
    const existingItem = await models.news.findById(id);

    if (!existingItem) {
      return res.status(404).send({ message: "خبر یافت نشد" });
    }

    await deleteRelatedCoverImage({
      slug: existingItem.slug,
      coverImageId: existingItem.coverImage,
    });
    await models.news.findByIdAndDelete(id);

    res.status(200).send({
      message: "خبر با موفقیت حذف شد",
    });
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در حذف خبر";
    return res.status(status).send({ message });
  }
};

export const PUBLIC_COVER = async (req, res) => {
  try {
    const { fileId } = req.params;

    if (!mongoose.Types.ObjectId.isValid(fileId)) {
      return res.status(404).send({ message: "تصویر یافت نشد" });
    }

    const coverExists = await models.news.exists({ coverImage: fileId });
    if (!coverExists) {
      return res.status(404).send({ message: "تصویر یافت نشد" });
    }

    const file = await models.file.findById(fileId);
    if (!file) {
      return res.status(404).send({ message: "تصویر یافت نشد" });
    }

    res.contentType(file.type || "application/octet-stream");
    return res.send(file.content);
  } catch (error) {
    const status = error.status || 500;
    const message = error.message || "خطا در دریافت تصویر خبر";
    return res.status(status).send({ message });
  }
};
