import {
  createReadStream,
  existsSync,
  mkdirSync,
  readFileSync,
  renameSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { createServer } from "node:http";
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { networkInterfaces } from "node:os";
import { dirname, extname, isAbsolute, join, normalize, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { WebSocket, WebSocketServer } from "ws";

const projectRoot = dirname(fileURLToPath(import.meta.url));
const dataDirectory = join(projectRoot, "data");
const databaseFile = join(dataDirectory, "db.json");

const allowedRootFiles = new Set(["index.html", "chat.html", "style.css", "script.js"]);
const allowedDirectories = new Set(["assets", "fonts"]);

const mimeTypes = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".svg": "image/svg+xml",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".webp": "image/webp",
  ".ttf": "font/ttf",
  ".woff2": "font/woff2",
};

const MAX_MESSAGES = 5000;
const avatarColors = ["avatar-blue", "avatar-pink", "avatar-purple", "avatar-mint", "avatar-gold"];

function emptyDatabase() {
  return { users: [], messages: [] };
}

function readDatabase() {
  if (!existsSync(databaseFile)) return emptyDatabase();
  try {
    const parsed = JSON.parse(readFileSync(databaseFile, "utf8"));
    return {
      users: Array.isArray(parsed.users) ? parsed.users : [],
      messages: Array.isArray(parsed.messages) ? parsed.messages : [],
    };
  } catch {
    return emptyDatabase();
  }
}

function writeDatabase(database) {
  mkdirSync(dataDirectory, { recursive: true });
  const temporaryFile = `${databaseFile}.tmp`;
  writeFileSync(temporaryFile, JSON.stringify(database, null, 2), "utf8");
  renameSync(temporaryFile, databaseFile);
}

function safeText(value, maxLength) {
  return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
}

function hashPassword(password, salt = randomBytes(16).toString("hex")) {
  return { salt, hash: scryptSync(password, salt, 64).toString("hex") };
}

function verifyPassword(password, user) {
  const candidate = Buffer.from(scryptSync(password, user.salt, 64).toString("hex"));
  const stored = Buffer.from(user.hash);
  return candidate.length === stored.length && timingSafeEqual(candidate, stored);
}

function publicUser(user) {
  return {
    id: user.id,
    username: user.username,
    name: user.name,
    initial: user.name.charAt(0) || user.username.charAt(0),
    color: user.color,
    createdAt: user.createdAt,
  };
}

function readJsonBody(request, limit = 8 * 1024) {
  return new Promise((resolveBody) => {
    let raw = "";
    let aborted = false;
    request.on("data", (chunk) => {
      if (aborted) return;
      raw += chunk;
      if (raw.length > limit) {
        aborted = true;
        resolveBody(null);
        request.destroy();
      }
    });
    request.on("end", () => {
      if (aborted) return;
      try {
        resolveBody(JSON.parse(raw || "{}"));
      } catch {
        resolveBody(null);
      }
    });
    request.on("error", () => resolveBody(null));
  });
}

function getStaticFile(pathname) {
  let decodedPath;
  try {
    decodedPath = decodeURIComponent(pathname);
  } catch {
    return null;
  }

  const requested = decodedPath === "/" ? "index.html" : decodedPath.replace(/^\/+/, "");
  const normalized = normalize(requested);
  if (
    !normalized ||
    normalized.startsWith(`..${sep}`) ||
    normalized === ".." ||
    isAbsolute(normalized)
  ) {
    return null;
  }

  const segments = normalized.split(/[\\/]/);
  const allowed =
    (segments.length === 1 && allowedRootFiles.has(segments[0])) ||
    (segments.length > 1 && allowedDirectories.has(segments[0]));
  if (!allowed) return null;

  const filePath = resolve(projectRoot, normalized);
  if (!filePath.startsWith(`${projectRoot}${sep}`) || !existsSync(filePath)) return null;
  if (!statSync(filePath).isFile()) return null;
  return filePath;
}

function getLanAddresses() {
  return Object.values(networkInterfaces())
    .flat()
    .filter(
      (address) =>
        address &&
        address.family === "IPv4" &&
        !address.internal &&
        !address.address.startsWith("169.254."),
    )
    .map((address) => address.address);
}

export async function createChatServer({ host = "0.0.0.0", port = 5099, log = false } = {}) {
  const database = readDatabase();
  const tokens = new Map();
  const connections = new Map();

  const findUserById = (id) => database.users.find((user) => user.id === id);
  const findUserByUsername = (username) =>
    database.users.find((user) => user.username === username.toLocaleLowerCase("en-US"));

  const issueToken = (user) => {
    const token = randomBytes(24).toString("hex");
    tokens.set(token, user.id);
    return token;
  };

  const isOnline = (userId) => (connections.get(userId)?.size || 0) > 0;

  const directory = () =>
    database.users.map((user) => ({ ...publicUser(user), online: isOnline(user.id) }));

  const sendTo = (userId, payload) => {
    const serialized = JSON.stringify(payload);
    for (const socket of connections.get(userId) || []) {
      if (socket.readyState === WebSocket.OPEN) socket.send(serialized);
    }
  };

  const broadcastDirectory = () => {
    const payload = { type: "directory", users: directory() };
    for (const userId of connections.keys()) sendTo(userId, payload);
  };

  const sendJson = (response, status, payload) => {
    response.writeHead(status, {
      "Content-Type": "application/json; charset=utf-8",
      "Cache-Control": "no-store",
    });
    response.end(JSON.stringify(payload));
  };

  const server = createServer(async (request, response) => {
    const url = new URL(request.url || "/", "http://localhost");

    if (url.pathname.startsWith("/api/")) {
      if (url.pathname === "/api/register" && request.method === "POST") {
        const body = await readJsonBody(request);
        const username = safeText(body?.username, 32).toLocaleLowerCase("en-US");
        const password = typeof body?.password === "string" ? body.password : "";
        const name = safeText(body?.name, 40) || username;

        if (!/^[a-z0-9_.]{3,32}$/.test(username)) {
          sendJson(response, 400, {
            error: "نام کاربری باید ۳ تا ۳۲ کاراکتر انگلیسی، عدد، نقطه یا زیرخط باشد.",
          });
          return;
        }
        if (password.length < 4) {
          sendJson(response, 400, { error: "رمز عبور باید حداقل ۴ کاراکتر باشد." });
          return;
        }
        if (findUserByUsername(username)) {
          sendJson(response, 409, { error: "این نام کاربری قبلاً ثبت شده است." });
          return;
        }

        const { salt, hash } = hashPassword(password);
        const user = {
          id: randomBytes(8).toString("hex"),
          username,
          name,
          salt,
          hash,
          color: avatarColors[database.users.length % avatarColors.length],
          createdAt: Date.now(),
        };
        database.users.push(user);
        writeDatabase(database);
        broadcastDirectory();
        sendJson(response, 201, { token: issueToken(user), user: publicUser(user) });
        return;
      }

      if (url.pathname === "/api/login" && request.method === "POST") {
        const body = await readJsonBody(request);
        const username = safeText(body?.username, 32).toLocaleLowerCase("en-US");
        const password = typeof body?.password === "string" ? body.password : "";
        const user = username ? findUserByUsername(username) : null;
        if (!user || !verifyPassword(password, user)) {
          sendJson(response, 401, { error: "نام کاربری یا رمز عبور درست نیست." });
          return;
        }
        sendJson(response, 200, { token: issueToken(user), user: publicUser(user) });
        return;
      }

      if (url.pathname === "/api/session" && request.method === "GET") {
        const token = (request.headers.authorization || "").replace(/^Bearer\s+/i, "");
        const user = findUserById(tokens.get(token));
        if (!user) {
          sendJson(response, 401, { error: "نشست معتبر نیست." });
          return;
        }
        sendJson(response, 200, { user: publicUser(user), users: directory() });
        return;
      }

      sendJson(response, 404, { error: "مسیر پیدا نشد." });
      return;
    }

    const filePath = getStaticFile(url.pathname);
    if (!filePath) {
      response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
      response.end("صفحه پیدا نشد.");
      return;
    }

    response.writeHead(200, {
      "Content-Type": mimeTypes[extname(filePath).toLowerCase()] || "application/octet-stream",
      "Cache-Control": "no-cache",
      "X-Content-Type-Options": "nosniff",
    });
    createReadStream(filePath).pipe(response);
  });

  const sockets = new WebSocketServer({ server, path: "/ws", maxPayload: 64 * 1024 });

  sockets.on("connection", (socket, request) => {
    const url = new URL(request.url || "/ws", "http://localhost");
    const user = findUserById(tokens.get(url.searchParams.get("token") || ""));
    if (!user) {
      socket.close(1008, "Invalid session");
      return;
    }

    const userId = user.id;
    if (!connections.has(userId)) connections.set(userId, new Set());
    connections.get(userId).add(socket);

    socket.send(
      JSON.stringify({
        type: "init",
        me: publicUser(user),
        users: directory(),
        messages: database.messages.filter(
          (message) => message.from === userId || message.to === userId,
        ),
      }),
    );
    broadcastDirectory();

    socket.on("message", (rawMessage) => {
      let payload;
      try {
        payload = JSON.parse(rawMessage.toString());
      } catch {
        return;
      }

      if (payload.type === "message" && payload.message) {
        const peerId = safeText(payload.to, 40);
        if (!findUserById(peerId) || peerId === userId) return;

        const incoming = payload.message;
        const kind = ["file", "voice"].includes(incoming.type) ? incoming.type : "text";
        const message = {
          id: safeText(incoming.id, 90) || `${userId}-${Date.now()}`,
          from: userId,
          to: peerId,
          type: kind,
          time: safeText(incoming.time, 24) || "اکنون",
          createdAt: Date.now(),
        };

        if (kind === "file") {
          message.fileName = safeText(incoming.fileName, 160) || "فایل";
          message.fileSize = safeText(incoming.fileSize, 80);
        } else if (kind === "voice") {
          message.duration = safeText(incoming.duration, 20) || "۰۰:۰۱";
        } else {
          message.text = safeText(incoming.text, 2000);
          if (!message.text) return;
        }

        if (incoming.reply && typeof incoming.reply === "object") {
          message.reply = {
            name: safeText(incoming.reply.name, 60),
            text: safeText(incoming.reply.text, 240),
          };
        }

        if (database.messages.some((item) => item.id === message.id)) return;
        database.messages.push(message);
        if (database.messages.length > MAX_MESSAGES) {
          database.messages = database.messages.slice(-MAX_MESSAGES);
        }
        writeDatabase(database);
        sendTo(userId, { type: "message", message });
        sendTo(peerId, { type: "message", message });
        return;
      }

      if (payload.type === "reaction") {
        const messageId = safeText(payload.messageId, 90);
        const message = database.messages.find((item) => item.id === messageId);
        if (!message || (message.from !== userId && message.to !== userId)) return;
        message.reaction = safeText(payload.reaction, 8) || undefined;
        writeDatabase(database);
        const update = { type: "reaction", messageId, reaction: message.reaction };
        sendTo(message.from, update);
        sendTo(message.to, update);
        return;
      }

      if (payload.type === "typing") {
        const peerId = safeText(payload.to, 40);
        if (!findUserById(peerId)) return;
        sendTo(peerId, { type: "typing", from: userId, active: Boolean(payload.active) });
        return;
      }

      if (payload.type === "clear") {
        const peerId = safeText(payload.to, 40);
        if (!findUserById(peerId)) return;
        database.messages = database.messages.filter(
          (message) =>
            !(
              (message.from === userId && message.to === peerId) ||
              (message.from === peerId && message.to === userId)
            ),
        );
        writeDatabase(database);
        const update = { type: "clear", from: userId, to: peerId };
        sendTo(userId, update);
        sendTo(peerId, update);
      }
    });

    socket.on("close", () => {
      const set = connections.get(userId);
      set?.delete(socket);
      if (set && set.size === 0) connections.delete(userId);
      broadcastDirectory();
    });
  });

  await new Promise((resolveListen, rejectListen) => {
    server.once("error", rejectListen);
    server.listen(port, host, () => {
      server.off("error", rejectListen);
      resolveListen();
    });
  });

  const address = server.address();
  const actualPort = typeof address === "object" && address ? address.port : port;

  if (log) {
    console.log(`Cobalt messenger is running on http://127.0.0.1:${actualPort}`);
    for (const lanAddress of getLanAddresses()) {
      console.log(`LAN: http://${lanAddress}:${actualPort}`);
    }
    console.log(`${database.users.length} registered account(s).`);
  }

  return {
    server,
    sockets,
    port: actualPort,
    async close() {
      for (const client of sockets.clients) client.terminate();
      await new Promise((resolveClose) => sockets.close(() => resolveClose()));
      await new Promise((resolveClose) => server.close(() => resolveClose()));
    },
  };
}

const isDirectRun =
  process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));

if (isDirectRun) {
  createChatServer({ port: Number(process.env.PORT) || 5099, log: true }).catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
}
