import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { createServer } from "node:http";
import { extname, resolve, sep } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const ROOT_DIRECTORY = fileURLToPath(new URL(".", import.meta.url));
const ENV_FILE = resolve(ROOT_DIRECTORY, ".env");
const MAX_BODY_SIZE = 16 * 1024;

try {
  process.loadEnvFile(ENV_FILE);
} catch (error) {
  if (error.code !== "ENOENT") throw error;
}

const PORT = Number(process.env.PORT || 4173);
const HOST = process.env.HOST || "127.0.0.1";

const CONTENT_TYPES = {
  ".css": "text/css; charset=utf-8",
  ".html": "text/html; charset=utf-8",
  ".ico": "image/x-icon",
  ".jpeg": "image/jpeg",
  ".jpg": "image/jpeg",
  ".js": "text/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".mp3": "audio/mpeg",
  ".png": "image/png",
  ".svg": "image/svg+xml",
  ".webp": "image/webp",
};

const PRIZE_LABELS = {
  jackpot: "FREE RM10",
  big: "FREE RM9",
  lucky: "FREE RM8",
  none: "No prize",
};
const inviteRegistry = new Map();

function canonicalPhoneDigits(value) {
  const rawDigits = String(value || "").replace(/\D/g, "");
  return rawDigits.startsWith("60") ? `0${rawDigits.slice(2)}` : rawDigits;
}

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

function readJsonBody(request) {
  return new Promise((resolveBody, rejectBody) => {
    let body = "";

    request.setEncoding("utf8");
    request.on("data", (chunk) => {
      body += chunk;
      if (Buffer.byteLength(body, "utf8") > MAX_BODY_SIZE) {
        rejectBody(Object.assign(new Error("Request is too large."), { statusCode: 413 }));
        request.destroy();
      }
    });
    request.on("end", () => {
      try {
        resolveBody(JSON.parse(body || "{}"));
      } catch {
        rejectBody(Object.assign(new Error("Invalid JSON request."), { statusCode: 400 }));
      }
    });
    request.on("error", rejectBody);
  });
}

function normalizeTelegramPayload(payload) {
  const event = String(payload.event || "daily_results").trim();
  const name = String(payload.name || "").trim().replace(/\s+/g, " ");
  const phone = String(payload.phone || "").trim().replace(/\s+/g, " ");
  const memberUsername = String(payload.memberUsername || "").trim().replace(/\s+/g, " ");
  const rawPhoneDigits = phone.replace(/\D/g, "");
  const phoneDigits = canonicalPhoneDigits(phone);
  const inviteCode = String(payload.inviteCode || "").trim().toUpperCase();

  if (name.length < 2 || name.length > 120) {
    throw Object.assign(new Error("Invalid player name."), { statusCode: 400 });
  }
  if (
    !/^[+0-9() .-]+$/.test(phone) ||
    (phone.startsWith("+") && !rawPhoneDigits.startsWith("60")) ||
    !/^01(?:0|1|2|3|4|6|7|8|9)\d{6,8}$/.test(phoneDigits)
  ) {
    throw Object.assign(new Error("Invalid Malaysian mobile number."), {
      statusCode: 400,
    });
  }
  if (inviteCode && !/^[A-Z0-9]{6,16}$/.test(inviteCode)) {
    throw Object.assign(new Error("Invalid invite code."), { statusCode: 400 });
  }
  if (
    ![
      "daily_results",
      "referral_used",
      "invite_registered",
      "claim_invite_bonus",
    ].includes(event)
  ) {
    throw Object.assign(new Error("Invalid Telegram event."), { statusCode: 400 });
  }

  if (
    event === "referral_used" ||
    event === "invite_registered" ||
    event === "claim_invite_bonus"
  ) {
    if (!inviteCode) {
      throw Object.assign(new Error("Invite code is required."), { statusCode: 400 });
    }
    return { event, name, memberUsername, phone, phoneDigits, inviteCode };
  }

  if (!Array.isArray(payload.results) || payload.results.length < 1 || payload.results.length > 10) {
    throw Object.assign(new Error("Invalid daily roll results."), { statusCode: 400 });
  }

  const results = payload.results.map((rawResult) => {
    const dice = Array.isArray(rawResult?.dice) ? rawResult.dice.map(Number) : [];
    const prize = String(rawResult?.prize || "");
    if (
      dice.length !== 2 ||
      dice.some((value) => !Number.isInteger(value) || value < 1 || value > 6)
    ) {
      throw Object.assign(new Error("Invalid dice result."), { statusCode: 400 });
    }
    if (!Object.hasOwn(PRIZE_LABELS, prize)) {
      throw Object.assign(new Error("Invalid prize result."), { statusCode: 400 });
    }
    return {
      dice,
      total: dice[0] + dice[1],
      prize,
      prizeLabel: PRIZE_LABELS[prize],
    };
  });

  return {
    event,
    name,
    phone,
    memberUsername,
    inviteCode,
    results,
  };
}

function escapeTelegramHtml(value) {
  return String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function buildTelegramMessage(payload) {
  const formattedTime = new Intl.DateTimeFormat("en-MY", {
    dateStyle: "medium",
    timeStyle: "medium",
    timeZone: "Asia/Kuala_Lumpur",
  }).format(new Date());

  if (payload.event === "referral_used") {
    const inviter = payload.inviter || {};
    return [
      "<b>Game : Dice Roll</b>",
      "",
      "🔗 <b>Invite Link Used</b>",
      "",
      `🎟 <b>Inviter code:</b> ${escapeTelegramHtml(payload.inviteCode)}`,
      `🙋 <b>Inviter username:</b> ${escapeTelegramHtml(
        inviter.name || "Not registered",
      )}`,
      `☎️ <b>Inviter phone:</b> ${escapeTelegramHtml(
        inviter.phone || "Not registered",
      )}`,
      "",
      `👤 <b>New player:</b> ${escapeTelegramHtml(payload.name)}`,
      `📱 <b>New player phone:</b> ${escapeTelegramHtml(payload.phone)}`,
      "✅ <b>Action:</b> Completed a roll from the invite link",
      payload.inviteBonusAwarded
        ? "🎁 <b>Inviter reward:</b> +1 extra roll chance"
        : "🎁 <b>Inviter reward:</b> No new bonus (already counted, self-referral, or unregistered)",
      `🕒 <b>Time:</b> ${escapeTelegramHtml(formattedTime)}`,
    ].join("\n");
  }

  const lines = [
    "<b>Game : Dice Roll</b>",
    "",
    `🎲 <b>Daily Dice Results (${payload.results.length} Rolls)</b>`,
    "",
    `👤 <b>Player:</b> ${escapeTelegramHtml(payload.name)}`,
    `📱 <b>Phone:</b> ${escapeTelegramHtml(payload.phone)}`,
    `🪪 <b>Member username:</b> ${escapeTelegramHtml(payload.memberUsername || "Not provided")}`,
  ];

  if (payload.inviteCode) {
    lines.push(`🎟 <b>Referred by:</b> ${escapeTelegramHtml(payload.inviteCode)}`);
  }

  payload.results.forEach((result, index) => {
    lines.push(
      "",
      `🎯 <b>Roll ${index + 1}:</b> ${result.dice[0]} + ${result.dice[1]} = ${result.total}`,
      `🏆 <b>Result:</b> ${escapeTelegramHtml(result.prizeLabel)}`,
    );
  });
  lines.push("", `🕒 <b>Time:</b> ${escapeTelegramHtml(formattedTime)}`);
  return lines.join("\n");
}

async function sendTelegramMessage(message) {
  const botToken = process.env.TELEGRAM_BOT_TOKEN;
  const chatId = process.env.TELEGRAM_CHAT_ID;

  if (!botToken || !chatId) {
    throw Object.assign(new Error("Telegram is not configured on the server."), {
      statusCode: 503,
    });
  }

  const telegramResponse = await fetch(
    `https://api.telegram.org/bot${botToken}/sendMessage`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chat_id: chatId,
        text: message,
        parse_mode: "HTML",
        disable_web_page_preview: true,
      }),
      signal: AbortSignal.timeout(10000),
    },
  );
  const telegramResult = await telegramResponse.json().catch(() => null);

  if (!telegramResponse.ok || !telegramResult?.ok) {
    const telegramDescription =
      typeof telegramResult?.description === "string"
        ? telegramResult.description.trim().slice(0, 180)
        : "";
    throw Object.assign(
      new Error(
        telegramDescription
          ? `Telegram: ${telegramDescription}`
          : "Telegram rejected the message.",
      ),
      { statusCode: 502 },
    );
  }
}

async function handleTelegramRoll(request, response) {
  try {
    const payload = await readJsonBody(request);
    const normalizedPayload = normalizeTelegramPayload(payload);

    if (normalizedPayload.event === "invite_registered") {
      const existingInvite = inviteRegistry.get(normalizedPayload.inviteCode) || {};
      inviteRegistry.set(normalizedPayload.inviteCode, {
        name: normalizedPayload.name,
        phone: normalizedPayload.phone,
        bonusRolls: Math.max(0, Number(existingInvite.bonusRolls) || 0),
        successfulInvitePhones:
          existingInvite.successfulInvitePhones instanceof Set
            ? existingInvite.successfulInvitePhones
            : new Set(),
      });
      if (inviteRegistry.size > 500) {
        inviteRegistry.delete(inviteRegistry.keys().next().value);
      }
      sendJson(response, 200, { ok: true });
      return;
    }

    const inviter = inviteRegistry.get(normalizedPayload.inviteCode) || null;
    if (normalizedPayload.event === "claim_invite_bonus") {
      const ownerMatches =
        inviter &&
        canonicalPhoneDigits(inviter.phone) === normalizedPayload.phoneDigits;
      const pendingBonusRolls = ownerMatches
        ? Math.max(0, Math.floor(Number(inviter.bonusRolls) || 0))
        : 0;
      const bonusRolls = pendingBonusRolls > 0 ? 1 : 0;
      if (ownerMatches && bonusRolls === 1) {
        inviter.bonusRolls = pendingBonusRolls - 1;
      }
      sendJson(response, 200, { ok: true, bonusRolls });
      return;
    }

    if (normalizedPayload.event === "referral_used") {
      normalizedPayload.inviter = inviter;
      normalizedPayload.inviteBonusAwarded = false;
      if (inviter) {
        if (!(inviter.successfulInvitePhones instanceof Set)) {
          inviter.successfulInvitePhones = new Set();
        }
        const isSelfReferral =
          canonicalPhoneDigits(inviter.phone) === normalizedPayload.phoneDigits;
        const alreadyRewarded = inviter.successfulInvitePhones.has(
          normalizedPayload.phoneDigits,
        );
        if (!isSelfReferral && !alreadyRewarded) {
          inviter.successfulInvitePhones.add(normalizedPayload.phoneDigits);
          inviter.bonusRolls = Math.max(0, Number(inviter.bonusRolls) || 0) + 1;
          normalizedPayload.inviteBonusAwarded = true;
        }
      }
    }
    await sendTelegramMessage(buildTelegramMessage(normalizedPayload));
    sendJson(response, 200, {
      ok: true,
      inviteBonusAwarded: Boolean(normalizedPayload.inviteBonusAwarded),
    });
  } catch (error) {
    const statusCode = Number(error.statusCode) || 500;
    const publicMessage =
      statusCode >= 500 ? error.message || "Telegram delivery failed." : error.message;
    sendJson(response, statusCode, { ok: false, message: publicMessage });
  }
}

async function serveStaticFile(request, response, pathname) {
  const relativePath = pathname === "/" ? "index.html" : `.${decodeURIComponent(pathname)}`;
  let filePath = resolve(ROOT_DIRECTORY, relativePath);
  const allowedRoot = ROOT_DIRECTORY.endsWith(sep) ? ROOT_DIRECTORY : `${ROOT_DIRECTORY}${sep}`;

  if (filePath !== resolve(ROOT_DIRECTORY, "index.html") && !filePath.startsWith(allowedRoot)) {
    response.writeHead(403);
    response.end("Forbidden");
    return;
  }

  try {
    const fileStats = await stat(filePath);
    if (fileStats.isDirectory()) filePath = resolve(filePath, "index.html");
    const contentType = CONTENT_TYPES[extname(filePath).toLowerCase()] || "application/octet-stream";
    response.writeHead(200, {
      "Cache-Control": "no-cache",
      "Content-Length": (await stat(filePath)).size,
      "Content-Type": contentType,
    });
    if (request.method === "HEAD") {
      response.end();
      return;
    }
    createReadStream(filePath).pipe(response);
  } catch {
    response.writeHead(404);
    response.end("Not found");
  }
}

const server = createServer(async (request, response) => {
  const requestUrl = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);

  if (
    requestUrl.pathname === "/api/telegram-roll" ||
    requestUrl.pathname === "/telegram-roll.php"
  ) {
    if (request.method !== "POST") {
      sendJson(response, 405, { ok: false, message: "Method not allowed." });
      return;
    }
    await handleTelegramRoll(request, response);
    return;
  }

  if (request.method !== "GET" && request.method !== "HEAD") {
    response.writeHead(405);
    response.end("Method not allowed");
    return;
  }

  await serveStaticFile(request, response, requestUrl.pathname);
});

server.listen(PORT, HOST, () => {
  console.log(`Dice game running at http://${HOST}:${PORT}`);
  if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) {
    console.log("Telegram delivery is disabled until TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are set.");
  }
});
