const { createHmac, timingSafeEqual, randomBytes } = require("node:crypto");
const { createServer } = require("node:http");
const { PrismaClient } = require("@prisma/client");
const next = require("next");
const { Server } = require("socket.io");

const dev = process.env.NODE_ENV !== "production";
const hostname = process.env.HOSTNAME || "0.0.0.0";
const port = Number(process.env.PORT || 3000);
const sessionCookie = "watchlink_session";
const prisma = new PrismaClient();

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
const presence = new Map();

app.prepare().then(() => {
  const httpServer = createServer((req, res) => handle(req, res));
  const io = new Server(httpServer, {
    path: "/api/socket",
    cors: {
      origin: process.env.NEXTAUTH_URL || "http://localhost:3000",
      credentials: true
    }
  });

  io.on("connection", (socket) => {
    socket.on("room:join", async ({ roomSlug } = {}, acknowledge) => {
      await safeSocket(socket, acknowledge, async () => {
        const session = await getSocketSession(socket);
        if (!session || !roomSlug) return reject(socket, "Sign in to join this room.", acknowledge);
        const context = await getRoomContext(roomSlug, session.user.id);
        if (!context.allowed) return reject(socket, "You do not have access to this room.", acknowledge);

        socket.data.user = session.user;
        socket.data.roomSlug = context.room.slug;
        socket.data.roomId = context.room.id;
        socket.join(context.room.slug);
        addPresence(context.room.slug, session.user, socket.id);

        socket.emit("room:state", await buildRoomSnapshot(context.room.id));
        io.to(context.room.slug).emit("presence:list", getPresence(context.room.slug));
        ok(acknowledge);
      });
    });

    socket.on("queue:add", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const sourceUrl = String(payload?.sourceUrl || "").trim();
      if (!sourceUrl) return;
      const settings = await getAppSettings();
      const media = normalizeMediaUrl(sourceUrl);
      if (!settings.allowedProviders.includes(media.provider)) return reject(socket, "This media provider is disabled.", acknowledge);

      const nextPosition = await prisma.mediaSource.count({ where: { roomId: room.id } });
      const created = await prisma.mediaSource.create({
        data: {
          roomId: room.id,
          submitterId: user.id,
          provider: media.provider,
          originalUrl: media.originalUrl,
          playbackUrl: media.playbackUrl,
          thumbnailUrl: media.thumbnailUrl,
          queuePosition: nextPosition + 1,
          title: media.title || media.originalUrl
        }
      });

      if (!room.currentState) {
        await persistPlaybackState(room.id, user, {
          mediaSourceId: created.id,
          status: "PAUSED",
          position: 0,
          rate: 1
        });
      }

      await audit("room.queue.add", user.id, room.id, { mediaSourceId: created.id, provider: media.provider });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("queue:play", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const mediaSourceId = String(payload?.mediaSourceId || "");
      const media = await prisma.mediaSource.findFirst({ where: { id: mediaSourceId, roomId: room.id } });
      if (!media) return;
      await persistPlaybackState(room.id, user, { mediaSourceId: media.id, status: "PLAYING", position: 0, rate: 1 });
      await audit("room.queue.play", user.id, room.id, { mediaSourceId: media.id });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("queue:remove", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const mediaSourceId = String(payload?.mediaSourceId || "");
      const media = await prisma.mediaSource.findFirst({ where: { id: mediaSourceId, roomId: room.id } });
      if (!media) return;
      await prisma.mediaSource.delete({ where: { id: media.id } });
      await normalizeQueue(room.id);

      const current = parseState(room.currentState);
      if (current?.mediaSourceId === media.id) {
        const nextMedia = await prisma.mediaSource.findFirst({
          where: { roomId: room.id },
          orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }]
        });
        await prisma.room.update({
          where: { id: room.id },
          data: {
            currentState: nextMedia
              ? playbackState(user, { mediaSourceId: nextMedia.id, status: "PAUSED", position: 0, rate: 1 })
              : null
          }
        });
      }

      await audit("room.queue.remove", user.id, room.id, { mediaSourceId: media.id });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("queue:move", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const mediaSourceId = String(payload?.mediaSourceId || "");
      const direction = payload?.direction === "down" ? 1 : -1;
      await moveMedia(room.id, mediaSourceId, direction);
      await audit("room.queue.move", user.id, room.id, { mediaSourceId, direction });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("playback:play", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      await persistPlaybackState(room.id, user, {
        mediaSourceId: String(payload?.mediaSourceId || parseState(room.currentState)?.mediaSourceId || ""),
        status: "PLAYING",
        position: Number(payload?.position || 0),
        rate: 1
      });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("playback:pause", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      await persistPlaybackState(room.id, user, {
        mediaSourceId: String(payload?.mediaSourceId || parseState(room.currentState)?.mediaSourceId || ""),
        status: "PAUSED",
        position: Number(payload?.position || 0),
        rate: 1
      });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("playback:seek", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const previous = parseState(room.currentState);
      await persistPlaybackState(room.id, user, {
        mediaSourceId: String(payload?.mediaSourceId || previous?.mediaSourceId || ""),
        status: previous?.status || "PAUSED",
        position: Number(payload?.position || 0),
        rate: 1
      });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("chat:message", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      const body = String(payload?.body || "").trim().slice(0, 1000);
      if (!body) return;
      await prisma.roomMessage.create({ data: { roomId: room.id, userId: user.id, body } });
      await audit("room.chat.message", user.id, room.id, {});
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("chat:delete", (payload, acknowledge) => safeRoomAction(socket, acknowledge, async ({ room, user }) => {
      if (!canManageRoom(room, user.id, user)) return reject(socket, "Only room managers can moderate chat.", acknowledge);
      const messageId = String(payload?.messageId || "");
      if (!messageId) return;
      const result = await prisma.roomMessage.deleteMany({ where: { id: messageId, roomId: room.id } });
      if (result.count === 0) return;
      await audit("room.chat.delete", user.id, room.id, { messageId });
      await broadcastRoom(io, room.slug, room.id);
    }));

    socket.on("disconnect", () => {
      const roomSlug = socket.data.roomSlug;
      const user = socket.data.user;
      if (roomSlug && user) {
        removePresence(roomSlug, socket.id);
        io.to(roomSlug).emit("presence:list", getPresence(roomSlug));
      }
    });
  });

  httpServer.listen(port, hostname, () => {
    console.log(`WatchLink ready on http://${hostname}:${port}`);
  });
});

async function safeSocket(socket, acknowledge, action) {
  try {
    await action();
  } catch (error) {
    console.error(error);
    reject(socket, "Realtime action failed.", acknowledge);
  }
}

async function safeRoomAction(socket, acknowledge, action) {
  await safeSocket(socket, acknowledge, async () => {
    const user = socket.data.user || (await getSocketSession(socket))?.user;
    const roomSlug = socket.data.roomSlug;
    if (!user || !roomSlug) return reject(socket, "Join a room before sending actions.", acknowledge);
    const context = await getRoomContext(roomSlug, user.id);
    if (!context.allowed) return reject(socket, "You do not have access to this room.", acknowledge);
    const result = await action({ room: context.room, user });
    if (result !== false) ok(acknowledge);
  });
}

function ok(callback) {
  if (typeof callback === "function") callback({ ok: true, at: Date.now() });
}

function reject(socket, message, callback) {
  socket.emit("room:error", { message });
  if (typeof callback === "function") callback({ ok: false, message, at: Date.now() });
  return false;
}

async function broadcastRoom(io, roomSlug, roomId) {
  io.to(roomSlug).emit("room:state", await buildRoomSnapshot(roomId));
}

async function getSocketSession(socket) {
  const cookies = parseCookies(socket.handshake.headers.cookie || "");
  const raw = cookies[sessionCookie];
  if (!raw) return null;
  const [userId, signature] = raw.split(".");
  if (!userId || !signature || !verifySignature(userId, signature)) return null;
  const user = await prisma.user.findUnique({
    where: { id: userId },
    include: { roles: { include: { role: true } } }
  });
  if (!user || user.disabledAt) return null;
  return { user };
}

function parseCookies(header) {
  return Object.fromEntries(
    header
      .split(";")
      .map((part) => part.trim())
      .filter(Boolean)
      .map((part) => {
        const index = part.indexOf("=");
        return [decodeURIComponent(part.slice(0, index)), decodeURIComponent(part.slice(index + 1))];
      })
  );
}

function verifySignature(value, signature) {
  const expected = createHmac("sha256", process.env.NEXTAUTH_SECRET || "development-only-change-me")
    .update(value)
    .digest("base64url");
  const expectedBuffer = Buffer.from(expected);
  const actualBuffer = Buffer.from(signature);
  return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
}

async function getRoomContext(slug, userId) {
  const room = await prisma.room.findUnique({
    where: { slug },
    include: {
      owner: true,
      members: { where: { userId }, select: { userId: true, canManage: true } }
    }
  });
  if (!room) return { allowed: false, room: null };
  const user = await prisma.user.findUnique({ where: { id: userId }, include: { roles: { include: { role: true } } } });
  const isAdmin = Boolean(user?.roles.some((userRole) => userRole.role.name === "admin"));
  const isOwner = room.ownerId === userId;
  const explicitMember = room.members.length > 0;
  const isFriend = room.ownerId
    ? Boolean(
        await prisma.friendship.findFirst({
          where: {
            status: "ACCEPTED",
            OR: [
              { requesterId: userId, receiverId: room.ownerId },
              { requesterId: room.ownerId, receiverId: userId }
            ]
          },
          select: { id: true }
        })
      )
    : false;
  const allowed =
    isAdmin ||
    isOwner ||
    room.visibility === "PUBLIC" ||
    (room.visibility === "FRIENDS" && (isFriend || explicitMember)) ||
    (room.visibility === "EXPLICIT" && explicitMember) ||
    (room.visibility === "ROLE_RESTRICTED" && explicitMember);
  return { allowed, room };
}

function canManageRoom(room, userId, user) {
  const isAdmin = Boolean(user?.roles?.some((userRole) => userRole.role.name === "admin"));
  return room.ownerId === userId || isAdmin || room.members.some((member) => member.userId === userId && member.canManage);
}

async function buildRoomSnapshot(roomId) {
  const room = await prisma.room.findUnique({
    where: { id: roomId },
    include: {
      mediaSources: {
        include: { submitter: true },
        orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }, { id: "asc" }],
        take: 60
      },
      messages: {
        include: { user: true },
        orderBy: { createdAt: "desc" },
        take: 50
      }
    }
  });
  if (!room) return null;
  const queue = room.mediaSources.map((item) => serializeMedia(item));
  const state = hydrateState(parseState(room.currentState), queue);
  return {
    roomId: room.id,
    roomSlug: room.slug,
    queue,
    playback: state,
    messages: room.messages
      .slice()
      .reverse()
      .map((message) => ({
        id: message.id,
        body: message.body,
        createdAt: message.createdAt.toISOString(),
        user: message.user?.displayName || message.user?.username || "Deleted user",
        avatarUrl: message.user?.avatarUrl || null
      }))
  };
}

function hydrateState(state, queue) {
  const current = queue.find((item) => item.id === state?.mediaSourceId) || queue[0] || null;
  if (!current) return null;
  const updatedAt = Number(state?.updatedAt || Date.now());
  const basePosition = Number(state?.position || 0);
  const status = state?.status === "PLAYING" ? "PLAYING" : "PAUSED";
  const livePosition = status === "PLAYING" ? basePosition + Math.max(0, Date.now() - updatedAt) / 1000 : basePosition;
  return {
    ...state,
    mediaSourceId: current.id,
    media: current,
    status,
    position: livePosition,
    rate: Number(state?.rate || 1),
    updatedAt
  };
}

function serializeMedia(item) {
  return {
    id: item.id,
    title: item.title || item.originalUrl,
    provider: item.provider,
    originalUrl: item.originalUrl,
    playbackUrl: item.playbackUrl,
    thumbnailUrl: item.thumbnailUrl,
    by: item.submitter?.displayName || item.submitter?.username || "Unknown",
    createdAt: new Intl.DateTimeFormat("en", { month: "short", day: "numeric" }).format(item.createdAt)
  };
}

async function persistPlaybackState(roomId, user, input) {
  if (!input.mediaSourceId) return;
  const media = await prisma.mediaSource.findFirst({ where: { id: input.mediaSourceId, roomId } });
  if (!media) return;
  await prisma.room.update({
    where: { id: roomId },
    data: {
      currentState: playbackState(user, input)
    }
  });
}

function playbackState(user, input) {
  return {
    mediaSourceId: input.mediaSourceId,
    status: input.status === "PLAYING" ? "PLAYING" : "PAUSED",
    position: Math.max(0, Number(input.position || 0)),
    rate: Number(input.rate || 1),
    updatedBy: user.username,
    updatedById: user.id,
    updatedAt: Date.now()
  };
}

function parseState(value) {
  return value && typeof value === "object" ? value : null;
}

async function moveMedia(roomId, mediaSourceId, direction) {
  const queue = await prisma.mediaSource.findMany({
    where: { roomId },
    orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }, { id: "asc" }],
    select: { id: true }
  });
  const index = queue.findIndex((item) => item.id === mediaSourceId);
  const target = index + direction;
  if (index < 0 || target < 0 || target >= queue.length) return;
  const reordered = [...queue];
  [reordered[index], reordered[target]] = [reordered[target], reordered[index]];
  await prisma.$transaction(
    reordered.map((item, itemIndex) =>
      prisma.mediaSource.update({ where: { id: item.id }, data: { queuePosition: itemIndex + 1 } })
    )
  );
}

async function normalizeQueue(roomId) {
  const queue = await prisma.mediaSource.findMany({
    where: { roomId },
    orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }, { id: "asc" }],
    select: { id: true }
  });
  await prisma.$transaction(
    queue.map((item, index) =>
      prisma.mediaSource.update({ where: { id: item.id }, data: { queuePosition: index + 1 } })
    )
  );
}

function addPresence(roomSlug, user, socketId) {
  const rows = presence.get(roomSlug) || new Map();
  const existing = rows.get(user.id);
  rows.set(user.id, {
    id: user.id,
    name: user.displayName || user.username,
    avatarUrl: user.avatarUrl || null,
    status: "Online",
    sockets: new Set([...(existing?.sockets || []), socketId])
  });
  presence.set(roomSlug, rows);
}

function removePresence(roomSlug, socketId) {
  const rows = presence.get(roomSlug);
  if (!rows) return;
  for (const user of rows.values()) {
    user.sockets.delete(socketId);
    if (user.sockets.size === 0) rows.delete(user.id);
  }
}

function getPresence(roomSlug) {
  return [...(presence.get(roomSlug)?.values() || [])].map(({ sockets, ...user }) => user);
}

async function getAppSettings() {
  const rows = await prisma.appSetting.findMany();
  const values = new Map(rows.map((row) => [row.key, row.value]));
  return {
    allowedProviders: (values.get("allowedProviders") || "YOUTUBE,TWITCH,DIRECT")
      .split(",")
      .map((item) => item.trim())
      .filter(Boolean)
  };
}

async function audit(action, actorId, roomId, metadata) {
  await prisma.auditEvent.create({ data: { action, actorId, roomId, metadata } }).catch(() => {});
}

function normalizeMediaUrl(input) {
  const originalUrl = input.trim();
  try {
    const url = new URL(originalUrl);
    const host = url.hostname.replace(/^www\./, "");
    if (host === "youtu.be") {
      return youtubeMedia(originalUrl, url.pathname.slice(1));
    }
    if (host.endsWith("youtube.com")) {
      return youtubeMedia(originalUrl, url.searchParams.get("v") || url.pathname.split("/").filter(Boolean).pop());
    }
    if (host.endsWith("twitch.tv")) {
      const parts = url.pathname.split("/").filter(Boolean);
      if (parts[0] === "videos" && parts[1]) {
        return {
          provider: "TWITCH",
          originalUrl,
          playbackUrl: `https://player.twitch.tv/?video=${parts[1]}&parent=localhost`,
          thumbnailUrl: "/icon.svg",
          title: originalUrl
        };
      }
      if (parts[0]) {
        return {
          provider: "TWITCH",
          originalUrl,
          playbackUrl: `https://player.twitch.tv/?channel=${parts[0]}&parent=localhost`,
          thumbnailUrl: "/icon.svg",
          title: originalUrl
        };
      }
    }
    if (/\.(mp4|webm|ogg|mov|m4v)(\?.*)?$/i.test(url.pathname)) {
      return { provider: "DIRECT", originalUrl, playbackUrl: originalUrl, thumbnailUrl: "/icon.svg", title: originalUrl };
    }
  } catch {}
  return { provider: "UNKNOWN", originalUrl, playbackUrl: originalUrl, thumbnailUrl: "/icon.svg", title: originalUrl };
}

function youtubeMedia(originalUrl, id) {
  const videoId = String(id || "").replace(/[^a-zA-Z0-9_-]/g, "");
  return {
    provider: videoId ? "YOUTUBE" : "UNKNOWN",
    originalUrl,
    playbackUrl: videoId ? `https://www.youtube.com/embed/${videoId}?enablejsapi=1&playsinline=1` : originalUrl,
    thumbnailUrl: videoId ? `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` : "/icon.svg",
    title: originalUrl
  };
}
