require("dotenv").config();
const http = require("http");
const express = require("express");
const { token } = process.env;
const { body } = require("express-validator");
const {
  Client,
  GatewayIntentBits,
  EmbedBuilder,
  AttachmentBuilder,
  REST,
} = require("discord.js");
const fs = require("fs");
const log4js = require("log4js");
const download = require("download");
const path = require("path");

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers, // <- privileged
    GatewayIntentBits.MessageContent, // <- privileged
    GatewayIntentBits.GuildMessages,
  ],
});

// Handle graceful shutdown
const cleanup = async () => {
  console.log("⚠️ Shutting down bot...");
  await client.destroy(); // cleanly disconnect from Discord Gateway
  process.exit(0);
};

process.on("SIGINT", cleanup); // Ctrl+C
process.on("SIGTERM", cleanup); // kill command or system shutdown

client.login(token);

const port = process.env.PORT || 3090;

const app = express();
const server = http.createServer(app);
app.use(express.json());

app.use(
  express.urlencoded({
    extended: true,
  })
);

log4js.configure({
  appenders: { system_log: { type: "file", filename: "system_log.log" } },
  categories: { default: { appenders: ["system_log"], level: "debug" } },
});

const logger = log4js.getLogger("system_log");

client.on("ready", () => console.log(`${client.user.tag} has logged in!`));

// Send message
app.post(
  "/send-message",
  [
    body("discord_id").notEmpty().withMessage("discord_id is required"),
    body("message").notEmpty().withMessage("message is required"),
    body("title").notEmpty().withMessage("title is required"),
    body("type").notEmpty().withMessage("type is required"),
    body("img").optional().isArray().withMessage("img must be an array"),
    body("files").optional().isArray().withMessage("files must be an array"),
    body("create_thread").optional().isBoolean(),
  ],
  async (req, res) => {
    const {
      discord_id,
      message,
      title,
      type,
      img = [],
      files = [],
      create_thread,
    } = req.body;

    logger.info(
      `Send Message | title: "${title}", discord_id: "${discord_id}", type: "${type}"`
    );

    try {
      if (type === "private") {
        const user = await client.users.fetch(discord_id).catch(() => null);

        if (!user) {
          logger.error(`User not found: ${discord_id}`);
          return res.status(404).json({
            status: false,
            error: "Couldn't find the user",
          });
        }

        logger.info(
          `Sending private message "${title}" to ${user.username} (ID: ${discord_id})`
        );

        const response = await send_message(
          discord_id,
          message,
          title,
          type,
          img,
          files
        );
        return res.status(200).json({
          status: true,
          response,
        });
      } else {
        logger.info(
          `Sending channel message "${title}" to channel ID: ${discord_id}`
        );

        const response = await send_message(
          discord_id,
          message,
          title,
          type,
          img,
          create_thread,
          files
        );
        return res.status(200).json({
          status: true,
          response,
        });
      }
    } catch (error) {
      logger.error(`Error sending message: ${error.message || error}`);
      return res.status(500).json({
        status: false,
        error: error.message || error,
      });
    }
  }
);

app.post("/send-post", async (req, res) => {
  const { title, channel_id, tags, images, files } = req.body;
  let { message } = req.body;

  console.log("----- Incoming Send Post Request Data -----");
  console.log("Title:", title);
  console.log("Channel ID:", channel_id);
  console.log("Tags:", tags);
  console.log("Images:", images);
  console.log("Files:", files);
  console.log("----- End of Request Data -----");

  // Validate the request body
  if (!title || !channel_id) {
    return res.status(400).send("Missing parameters: title, and/or channel_id");
  }

  try {
    // Get the channel by ID
    const channel = await client.channels.fetch(channel_id);
    if (!channel) {
      return res.status(404).send("Channel not found");
    }

    // Ensure the channel is a forum channel (or has necessary permissions)
    if (channel.type !== 15) {
      // 15 corresponds to a Forum Channel
      return res.status(400).send("Channel is not a forum channel");
    }

    // Prepare the tags, if provided
    let appliedTags = [];
    if (tags && Array.isArray(tags)) {
      appliedTags = tags; // Tags should be an array of tag IDs
    }

    // Prepare image and file attachments
    let attachments = [];
    if (images && Array.isArray(images)) {
      attachments.push(
        ...images.map((image) => ({
          attachment: image, // The URL or path to the image
        }))
      );
    }
    console.dir(images);
    // Optionally handle file attachments from the 'files' array
    if (files && Array.isArray(files)) {
      attachments.push(
        ...files.map((fileUrl) => ({
          attachment: fileUrl, // The URL to the file
        }))
      );
    }
    console.dir(files);
    const autoArchiveDuration = 10080; // Duration in minutes (7 days)

    if (message == "") {
      message = title;
    }

    // Create the forum post (thread) in the channel
    const thread = await channel.threads.create({
      name: title, // The title of the post
      autoArchiveDuration: autoArchiveDuration, // Archive after 7 days
      message: {
        content: message,
        files: attachments, // Attach images and files here
      },
      appliedTags: appliedTags,
      reason: `Forum post created`,
    });

    // Send a successful response with the thread ID
    res.status(200).json({
      status: true,
      response: thread.id,
    });
  } catch (error) {
    console.error(error);
    res.status(500).send("Failed to create forum post.");
  }
});

app.post("/edit-message", async (req, res) => {
  const { message_id, title, new_message, category_id } = req.body;

  // Validate input
  if (!message_id || (!title && !new_message) || !category_id) {
    return res.status(400).json({
      error: "message_id, and at least title or new_message are required.",
    });
  }
  console.log("Editing message.....searching for channel by category_id");
  try {
    let foundChannelId = null;
    const category = client.channels.cache.get(category_id);
    // for (const channel of category.children.cache.values()) {
    //   // Ensure the channel is text-based (can contain threads)
    //   if (channel.isTextBased()) {
    //     try {
    //       // Fetch threads for the channel
    //       console.log(channel.name);
    //       const allThreads = await channel.threads.fetch();
    //       const threads = allThreads.threads.filter(thread => thread.parentId === channel.id);

    //       threads.forEach(async (thread) => {
    //           try {
    //             const message = await channel.messages.fetch(message_id);
    //             foundChannelId = channel.id;
    //           } catch (error) {
    //               console.error(`Failed to delete thread: ${thread.name}`, error);
    //           }
    //       });
    //       //console.log(threads);
    //     } catch (error) {
    //       console.error(`Error fetching threads for channel ${channel.id}:`, error);
    //     }
    //   }
    // }
    for (const channel of category.children.cache.values()) {
      // Ensure the channel is text-based (can contain threads)
      if (channel.isTextBased()) {
        try {
          // Fetch threads for the channel
          console.log(`Checking threads in channel: ${channel.name}`);
          const allThreads = await channel.threads.fetch();
          const threads = allThreads.threads.filter(
            (thread) => thread.parentId === channel.id
          );

          threads.forEach(async (thread) => {
            try {
              console.log(`Thread Name : ${thread.name}`);
              // Fetch the message within the thread
              const message = await thread.messages.fetch(message_id);
              foundChannelId = thread.id;
              console.log(
                `Found message in thread: ${thread.name} in channel: ${channel.name}`
              );
              return;
            } catch (error) {
              //console.error(`Failed to fetch message in thread: ${thread.name} in channel: ${channel.name}`, error);
            }
          });
        } catch (error) {
          console.error(
            `Error fetching threads for channel ${channel.id}:`,
            error
          );
        }
      }
    }

    // Fetch the channel
    const final_channel = await client.channels.fetch(foundChannelId);
    if (!final_channel.isTextBased()) {
      return res
        .status(400)
        .json({ error: "The specified channel is not a text channel." });
    }

    // Fetch the message
    const message = await final_channel.messages.fetch(message_id);
    if (!message) {
      return res.status(404).json({ error: "Message not found." });
    }
    console.log("Channel id found....sending edited message....");
    // Extract existing components to preserve them
    const components = message.components;

    // Prepare the new embed
    const embed = new EmbedBuilder()
      .setTitle(title || message.embeds[0]?.title || "") // Use new title or keep the old one
      .setDescription(new_message || message.embeds[0]?.description || "") // Use new description or keep the old one
      .setColor(message.embeds[0]?.color || null); // Keep existing color if available

    // Edit the message
    await message.edit({ embeds: [embed], components });
    res.json({ success: true, message: "Message edited successfully." });
  } catch (error) {
    console.error("Error editing message:", error);
    res.status(500).json({ error: "Failed to edit message." });
  }
});

app.post("/change-post", async (req, res) => {
  const { channel_ids, tags } = req.body;
  logger.info("supplied ids:");
  console.dir(channel_ids);
  logger.info("supplied tags:");
  console.dir(tags);

  if (
    !channel_ids ||
    !Array.isArray(channel_ids) ||
    !tags ||
    !Array.isArray(tags)
  ) {
    return res
      .status(400)
      .send(
        "Invalid parameters: channel_ids and tags are required and should be arrays."
      );
  }

  if (channel_ids.length !== tags.length) {
    return res
      .status(400)
      .send(
        "Mismatched array lengths: channel_ids and tags arrays should be of the same length."
      );
  }

  const failedThreads = [];

  try {
    for (let i = 0; i < channel_ids.length; i++) {
      const threadId = channel_ids[i];
      const threadTags = tags[i];
      logger.info(`Running thru thread ID: ${threadId}`);
      logger.info("With tags:");
      logger.info(threadTags);

      try {
        const thread = await client.channels.fetch(threadId);
        const archived_status = thread.archived;

        if (!thread) {
          logger.warn(`Thread not found: ${threadId}`);
          failedThreads.push(threadId);
          continue;
        }

        if (thread.type !== 11) {
          logger.warn(`Specified ID is not a forum thread: ${threadId}`);
          failedThreads.push(threadId);
          continue;
        }

        if (archived_status) {
          await thread.setArchived(false);
          logger.info(`Unarchived thread ${threadId} for tag update`);
        }

        let appliedTags = [];
        if (threadTags && Array.isArray(threadTags)) {
          appliedTags = threadTags.filter(
            (tag) => typeof tag === "string" && /^\d{17,19}$/.test(tag)
          );
          logger.info(`Is appliedTags an array? ${Array.isArray(appliedTags)}`);
        } else {
          logger.warn("appliedTags are not an array");
        }

        logger.info("Applying tags:");
        logger.info(appliedTags);

        if (appliedTags.length === 0) {
          logger.warn(`No valid tags provided for thread: ${threadId}`);
          failedThreads.push(threadId);
          continue;
        }

        const sortedExistingTags = thread.appliedTags
          ? [...thread.appliedTags].sort()
          : [];
        const sortedAppliedTags = [...appliedTags].sort();

        if (
          JSON.stringify(sortedExistingTags) !==
          JSON.stringify(sortedAppliedTags)
        ) {
          await thread.setAppliedTags(appliedTags);

          const tagNames = await Promise.all(
            appliedTags.map(async (tagId) => {
              const tag = await thread.guild.channels.cache.get(tagId);
              return tag ? tag.name : `Unknown Tag (ID: ${tagId})`;
            })
          );

          logger.info(`Tags updated for thread ${thread.id} (${thread.name})`);
          logger.info("Current applied tags:", tagNames);
          logger.info("---------DONE TAG--------");
        } else {
          logger.info("----- SAME TAG, SKIPPED -----");
        }

        if (archived_status) {
          await thread.setArchived(true);
          logger.info(`Re-archived thread ${threadId}`);
        }
      } catch (threadError) {
        logger.error(`Error processing thread ${threadId}:`, threadError);
        failedThreads.push(threadId);
        continue;
      }
    }

    if (failedThreads.length > 0) {
      res.status(207).json({
        status: false,
        message: "Tags updated for some threads, but some failed.",
        failedThreads,
      });
    } else {
      res.status(200).json({
        status: true,
        message: "Tags updated for all specified threads.",
      });
    }
  } catch (error) {
    logger.error("General failure in tag update process:", error);
    res.status(500).send("Failed to update tags.");
  }
});

app.post("/delete-post", async (req, res) => {
  const { thread_ids } = req.body;

  // Validate that the required parameters are present
  if (!thread_ids || !Array.isArray(thread_ids)) {
    return res
      .status(400)
      .send(
        "Invalid parameters: thread_ids is required and should be an array."
      );
  }

  try {
    let deletedThreads = [];

    // Loop through each thread ID
    for (let threadId of thread_ids) {
      // Fetch the thread by ID
      const thread = await client.channels.fetch(threadId);

      const archived_status = thread.archived;

      if (!thread) {
        logger.warn(`Thread not found: ${threadId}`);
        return res.status(404).send(`Thread not found: ${threadId}`);
      }

      if (thread.type !== 11) {
        logger.warn(`Specified ID is not a forum thread: ${threadId}`);
        return res
          .status(400)
          .send(`Specified ID is not a forum thread: ${threadId}`);
      }

      if (archived_status) {
        await thread.setArchived(false);
        logger.info(`Unarchived thread ${threadId} for delete purpose`);
      }

      // Delete the thread
      await thread.delete();
      console.log(`Thread ${threadId} deleted successfully.`);
      logger.info(`Thread ${threadId} deleted successfully.`);
      deletedThreads.push(threadId);
    }

    // Respond to the client with success
    res.status(200).json({
      status: true,
      message: "Threads deleted successfully",
      deleted_threads: deletedThreads,
    });
  } catch (error) {
    console.error(error);
    res.status(500).send("Failed to delete threads.");
  }
});

const MAX_EMBED_LENGTH = 4096;

async function send_message(
  channel_id,
  msg,
  title,
  type,
  images = [],
  create_thread = false,
  files = []
) {
  logger.info(
    `send_message | id=${channel_id}, type=${type}, title="${title}"`
  );

  // --- Helper: Split long text into chunks that fit Discord embed limits ---
  const splitText = (text, maxLength) =>
    text.match(new RegExp(`.{1,${maxLength}}`, "gs")) || [];

  // --- Helper: Extract unique mentions (<@userID>) and remove them from text ---
  const extractMentions = (text) => {
    const regex = /<@.*?>/g;
    const matches = text.match(regex) || [];
    const unique = [...new Set(matches)];
    let mentionMsg = "";
    let cleaned = text;

    for (const mention of unique) {
      mentionMsg += ` ${mention}`;
      cleaned = cleaned.replace(new RegExp(mention, "g"), "");
    }
    return { mentionMsg: mentionMsg.trim(), cleaned };
  };

  // --- Helper: Extract all URLs and remove them from text ---
  const extractUrls = (text) => {
    const urlPattern = /https?:\/\/[^\s]+/g;
    const urls = text.match(urlPattern) || [];
    return {
      urls,
      cleaned: text
        .replace(urlPattern, "")
        .replace(/\s{2,}/g, " ")
        .trim(),
    };
  };

  // --- Helper: Send embeds in chunks (Discord limit is ~4096 chars per embed) ---
  const sendEmbeds = async (
    channel,
    title,
    fullText,
    mentionMsg,
    withThread = false
  ) => {
    const parts = splitText(fullText, MAX_EMBED_LENGTH);
    let lastMessage;

    for (const [i, part] of parts.entries()) {
      const embed = new EmbedBuilder()
        .setTitle(i === 0 ? title : `${title} (cont.)`) // only the first embed has the full title
        .setDescription(part)
        .setColor(0x18e1ee);

      // Send each part sequentially
      lastMessage = await channel.send({
        content: mentionMsg,
        embeds: [embed],
      });
    }

    // If requested, create a thread from the last embed message
    if (withThread && withThread !== "false" && withThread !== "0") {
      const thread = await lastMessage.startThread({
        name: title,
        type: 11, // Public thread
        autoArchiveDuration: 10080, // 7 days (max auto-archive duration)
        reason: "Created via API request",
      });
      return thread.id;
    }
  };

  // --- Helper: Send multiple attachments (images or files) ---
  const sendAttachments = async (channel, list) => {
    for (const file of list) {
      logger.info(`Sending attachment: ${file}`);
      const attachment = new AttachmentBuilder(file);
      await channel.send({ files: [attachment] });
    }
  };

  // --- MAIN LOGIC ---
  try {
    // Case 1: Sending to private DM
    if (type === "private") {
      const embed = new EmbedBuilder()
        .setTitle(title)
        .setDescription(msg)
        .setColor(0x18e1ee);

      await client.users.send(channel_id, { embeds: [embed] });
      return "sent";
    }

    // Case 2: Sending to a channel
    if (type === "channel") {
      const channel = client.channels.cache.get(channel_id);
      if (!channel) throw new Error(`Channel not found: ${channel_id}`);

      // Check if the message contains "----====" → split into multiple parts
      const msgParts = msg.includes("----====") ? msg.split("----====") : [msg];

      for (let part of msgParts) {
        // Extract mentions and clean text
        let { mentionMsg, cleaned } = extractMentions(part);

        // If it's a "note" message, also extract URLs (e.g., PDF links)
        let urls = [];
        if (
          title.toLowerCase().includes("note sebelum visit") ||
          title.toLowerCase().includes("note pergantian brand")
        ) {
          const urlExtract = extractUrls(cleaned);
          cleaned = urlExtract.cleaned;
          urls = urlExtract.urls;
        }

        // Send the message as embeds (optionally creating a thread)
        await sendEmbeds(channel, title, cleaned, mentionMsg, create_thread);

        // Send additional images & files if provided
        if (images.length > 0) await sendAttachments(channel, images);
        if (files.length > 0) await sendAttachments(channel, files);

        // Special handling: download & convert PDFs into images
        for (const pdfUrl of urls) {
          logger.info(`Processing PDF: ${pdfUrl}`);
          try {
            const filename = pdfUrl
              .split("/")
              .pop()
              .replace(/\.[^/.]+$/, "");
            const pdfPath = path.join(__dirname, `${filename}.pdf`);
            const outputDir = path.join(__dirname, `output_${filename}`);

            // Ensure output directory exists
            if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);

            // Download PDF from URL
            await download(pdfUrl, __dirname, { filename: `${filename}.pdf` });

            // Convert the PDF into images
            const imagePaths = await convertPdfToImages(pdfPath, outputDir);

            // Send the converted images to the channel
            await sendAttachments(channel, imagePaths);

            // Cleanup temporary files
            fs.unlinkSync(pdfPath);
            for (const imgPath of imagePaths) fs.unlinkSync(imgPath);
            if (fs.readdirSync(outputDir).length === 0) fs.rmdirSync(outputDir);
          } catch (err) {
            logger.error(`PDF processing failed: ${err}`);
            await channel.send("An error occurred while processing the PDF.");
          }
        }
      }

      return "sent";
    }
  } catch (err) {
    // Catch any error and re-throw so the caller can handle it
    logger.error(`send_message failed: ${err.stack || err}`);
    throw err;
  }
}

server.listen(port, function () {
  console.log("App running on *: " + port);
});
