const express = require("express");
const { createServer } = require("http");
const app = express();
const cors = require("cors");
const server = createServer(app);
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const bcrypt = require("bcrypt");
const saltRounds = 10;
const { Server } = require("socket.io");
const io = new Server(server);
const multer = require("multer");
const { join } = require("path");
const { existsSync, mkdirSync } = require("fs");
require("dotenv").config();
const PORT = process.env.SERVER_PORT;
const {
  insertUser,
  checkUserExist,
  getPassword,
  getUserId,
  deleteContact,
  updateContactStatus,
  getMessages,
} = require("./db/db.js");
const authorizeUser = require("./utils/authorize");
const {
  isValidUsername,
  MIN_USERNAME_LENGTH,
  MAX_USERNAME_LENGTH,
  MAX_PASSWORD_LENGTH,
  MIN_PASSWORD_LENGTH,
} = require("./utils/filter");
const { generateJwtToken } = require("./auth/jwt");
const { initializeSocket } = require("./socket/socket");
const {
  getContacts,
  insertContact,
  createGroup,
  addMemberToGroupByUsername,
  contactSuggestion,
  deleteMessage,
  getMembers,
  insertContactByUsername,
  isConversationMember,
  isAdmin,
} = require("./db/db");
const { extname } = require("node:path");

const corsOptions = {
  origin: process.env.ORIGIN,
  optionsSuccessStatus: 200,
  credentials: true,
};

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    const dir = join(__dirname, "attachments");
    if (!existsSync(dir)) {
      mkdirSync(dir, { recursive: true });
    }
    cb(null, dir);
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
    const extension = extname(file.originalname).toString();
    const originalName = file.originalname;
    const filename = originalName?.substring(0, originalName.lastIndexOf("."));
    const finalName = `${filename}-${uniqueSuffix}${extension}`;
    file.finalName = finalName;
    cb(null, finalName);
  },
});

const upload = multer({ storage: storage });

// Serve socket.io js
app.use("/socket.io", express.static("./node_modules/socket.io/client-dist/"));
app.use("/attachments", express.static("./attachments"));
app.use(cors(corsOptions));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(express.json({ limit: "100G" }));
app.use(express.urlencoded({ extended: true, limit: "100G" }));

app.post("/api/auth/signup", async (req, res) => {
  try {
    const username = req.body.username;
    const password = req.body.password;

    if (!username) {
      return res.status(400).json({ message: "No username provided" });
    } else if (!password) {
      return res.status(400).json({ message: "No password provided" });
    }

    // Check for invalid characters in password
    const validChars = /^[A-Za-z0-9!@#$%^&*(),.?":{}|<>]+$/;
    if (!validChars.test(password)) {
      return res
        .status(400)
        .json({ message: "Password contains invalid character" });
    }

    // Validate username for invalid characters, length, and type
    if (!isValidUsername(username)) {
      return res.status(400).json({ message: "Invalid username provided" });
    }

    // Validate form data length
    if (
      !password ||
      password.length < MIN_PASSWORD_LENGTH ||
      password.length > MAX_PASSWORD_LENGTH
    ) {
      return res.status(400).json({ message: "Invalid password length" });
    }

    // Checks if user already exist in database
    const exist = await checkUserExist(username);
    if (exist) {
      return res.status(409).json({ message: "User already exist" });
    }

    // Hash password and insert hash and username to database
    const hash = await bcrypt.hash(password, saltRounds);

    // Insert username and password hash to database
    await insertUser(username, hash);

    // Get user id from database to store it in jwt token
    const { user_id } = await getUserId(username);
    console.log(`Registered: ${username} with id: ${user_id}`);
    // Set JWT token to cookies
    const token = generateJwtToken(username, user_id);
    res.cookie("token", token, {
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
      // httpOnly: true,
      // secure: true,
    });

    return res.status(200).json({ message: "Successfully signed up" });
  } catch (e) {
    console.error("Signup error: ", e);
    return res.status(500).json({ message: "internal server error" });
  }
});

app.post("/api/auth/login", async (req, res) => {
  try {
    const username = req.body.username?.trim();
    const password = req.body.password;

    if (!isValidUsername(username)) {
      return res.status(400).json({ message: "Invalid credentials" });
    }

    if (
      !username ||
      !password ||
      username.length < MIN_USERNAME_LENGTH ||
      username.length > MAX_USERNAME_LENGTH ||
      password.length < MIN_PASSWORD_LENGTH ||
      password.length > MAX_PASSWORD_LENGTH
    ) {
      return res.status(400).json({ message: "Invalid credentials" });
    }

    // Checks if the user exist
    const exist = await checkUserExist(username);
    if (!exist) {
      return res.status(404).json({ message: "User does not exist" });
    }
    const hashedPassword = await getPassword(username);
    // Compare passwords
    bcrypt
      .compare(password, hashedPassword)
      .then(async (result) => {
        if (!result) {
          res.status(401).json({ message: "Invalid password" });
          return;
        }
        const { user_id, dbusername } = await getUserId(username);
        const token = generateJwtToken(dbusername, user_id);
        res.cookie("token", token, {
          maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
        });
        return res.status(200).json({ message: "Successfully logged In" });
      })
      .catch((e) => {
        console.error("Failed to compare password: ", e);
        return res.status(500).json({ message: "Internal server error" });
      });
  } catch (e) {
    console.error("Login error: ", e);
    return res.status(500).json({ message: "Internal server error" });
  }
});

app.get("/api/auth/validate", authorizeUser, (req, res) => {
  return res.status(200).json({
    message: "Authorized",
    username: req.user.username,
    user_id: req.user.user_id,
  });
});

app.delete(
  "/api/chat/contacts/:contact_id/:conversation_id",
  authorizeUser,
  async (req, res) => {
    const contact_id = parseInt(req.params.contact_id);
    const conversation_id = parseInt(req.params.conversation_id);

    console.log("Delete contact for: ", req.params.contact_id, conversation_id);

    const result = await deleteContact(
      req.user.user_id,
      contact_id,
      conversation_id,
    );
    if (result?.message) {
      return res.status(400).json({ message: result.message });
    }
    return res.status(200).json({ message: "Successfully deleted contact" });
  },
);

app.put(
  "/api/chat/contacts/:conversation_id",
  authorizeUser,
  async (req, res) => {
    const conversation_id = req.params.conversation_id;
    if (!conversation_id) {
      return res
        .status(400)
        .json({ message: "Missing conversation id parameter" });
    }

    const read = req.body.status;
    await updateContactStatus(req.user.user_id, conversation_id, read);

    return res
      .status(200)
      .json({ message: `Successfully updated contact status to: ${read}` });
  },
);

app.post("/api/chat/contact/:contact", authorizeUser, async (req, res) => {
  if (!req.params.contact) {
    return res.status(400).json({ message: "Missing contact parameter" });
  }
  const contactUsername = req.params.contact;
  // Validate username for invalid characters, length, and type
  if (!isValidUsername(contactUsername)) {
    return res.status(400).json({ message: "Invalid username provided" });
  }
  // Get contact user id and check if exist
  const contact = await getUserId(contactUsername);
  if (!contact) {
    return res.status(400).json({ message: "User does not exist" });
  }

  try {
    const result = await insertContact(
      req.user.user_id,
      contact.user_id,
      contactUsername,
      true,
    );
    if (result === null) {
      return res.status(500).json({ message: "Failed to create conversation" });
    }
    if (!result.user_id) {
      return res.status(500).json({ message: "Something went wrong" });
    }
    console.log("sent contact post response: ", result);
    return res.status(200).json(result);
  } catch (e) {
    console.error("Failed to insert contact: ", e);
    return res.status(500).json({ message: "Failed to insert contact" });
  }
});

app.get("/api/chat/contacts", authorizeUser, async (req, res) => {
  if (!req.user.user_id) {
    return res.status(401).json({
      message: "Missing username (that's weird you shouldn't see that)",
    });
  }
  const contacts = await getContacts(req.user.user_id);
  console.log("Sent contacts list for: ", req.user.user_id);
  return res.status(200).json(contacts);
});

app.get(
  "/api/chat/contacts/suggestions/:contact",
  authorizeUser,
  async (req, res) => {
    const contact = req.params.contact;
    if (!contact) {
      return res.status(400).json({
        message: "contact not provided",
      });
    }
    try {
      const suggestions = await contactSuggestion(contact);
      return res.status(200).json(suggestions);
    } catch (e) {
      res.status(500).json({ message: "Failed to get contact suggestions" });
    }
  },
);

app.get("/api/chat/messages/:contact", authorizeUser, async (req, res) => {
  if (!req.params.contact) {
    return res.status(400).json({ message: "Missing contact parameter" });
  }
  const limit = parseInt(req.query.limit) || 50;
  const cursor = parseInt(req.query.cursor) || 0;

  const messages = await getMessages(
    req.user.user_id,
    req.params.contact,
    limit,
    cursor,
  );
  if (messages && messages.length === 0) {
    return res
      .status(200)
      .json({ messages: [], message: "No more messages found" });
  }

  console.log("Sent messages for: ", req.user.user_id);
  return res.status(200).json({ messages });
});

app.post(
  "/api/chat/attachments",
  upload.any("attachments"),
  authorizeUser,
  async (req, res) => {
    if (req.files?.length < 1) {
      return res.status(400).json({ message: "No file specified" });
    }
    const attachment_urls = req.files.map((file) => {
      return `${process.env.ORIGIN}/attachments/${file.finalName}`;
    });
    res.json(attachment_urls);
  },
);

app.post("/api/chat/groups/create", authorizeUser, async (req, res) => {
  const user_id = req.user.user_id;
  const groupname = req.body.groupName;
  if (!groupname) {
    return res.status(400).json({ message: "Groupname not provided" });
  }
  const { group_id, contact_user_id, errorMessage } = await createGroup(
    user_id,
    groupname,
  );
  if (errorMessage) {
    return res.status(401).json({ message: errorMessage });
  }
  if (!group_id) {
    return res.status(500).json({ message: "Failed to create group" });
  }
  console.log("Successfully created group: ", groupname, "id: ", group_id);
  console.log(`io.to: ${contact_user_id} added to group`);
  io.to(contact_user_id).emit("added to group", { group_id });
  return res.status(200).json({
    message: `Successfully created group: ${groupname}`,
    group_id: group_id,
  });
});

app.post("/api/chat/groups/addMember", authorizeUser, async (req, res) => {
  const username = req.body.username;
  const group_id = req.body.group_id;
  const user_id = req.user.user_id;
  if (!username) {
    return res.status(400).json({ message: "Username not provided" });
  }
  if (!group_id) {
    return res.status(400).json({ message: "group_id not provided" });
  }

  const isUserAdmin = await isAdmin(user_id, group_id);
  if (!isUserAdmin) {
    return res.status(401).json({ message: "You are not group administrator" });
  }

  const result = await addMemberToGroupByUsername(group_id, username);
  if (result !== null) {
    io.to(result).to(group_id).emit("added to group", {
      username,
      user_id: result,
      group_id,
    });
    console.log("added to group: ", result);
    return res.status(200).json({ message: "Successfully added member" });
  }
  res.status(500).json({ message: "Failed to add member" });
});

app.delete(
  "/api/chat/messages/:message_id",
  authorizeUser,
  async (req, res) => {
    const message_id = req.params.message_id;
    console.log("delete message: ", req.user.user_id, message_id);
    if (!message_id) {
      return res.status(500).json({ message: "No message_id provided" });
    }

    const result = await deleteMessage(req.user.user_id, message_id);
    if (result?.message) {
      return res.status(401).json({ message: result.message });
    } else {
      return res.status(200).json({ message: "Successfully deleted message" });
    }
  },
);

app.get(
  "/api/chat/groups/getMembers/:conversation_id",
  authorizeUser,
  async (req, res) => {
    const conversation_id = req.params.conversation_id;
    if (!conversation_id) {
      return res.status(400).json({ message: "No conversation_id provided" });
    }

    const isMember = await isConversationMember(
      req.user.user_id,
      conversation_id,
    );
    if (!isMember) {
      return res
        .status(401)
        .json({ message: "You are not member of this conversation" });
    }

    const participants = await getMembers(conversation_id);
    console.log(
      `getMemers for conversation: ${conversation_id}, participants: ${participants} `,
    );
    if (participants) {
      return res.status(200).json(participants);
    } else {
      return res.status(500).json({ message: "Failed to get group members" });
    }
  },
);

app.get(
  "/api/chat/messages/lastMessage",
  authorizeUser,
  async (req, res) => {},
);

initializeSocket(io);

server.listen(PORT, () => {
  console.log(`Server is running on port: ${PORT}`);
});
