import { Server, Socket } from "socket.io";
import {
  assertConversationParticipant,
  persistMessage,
  markConversationRead,
} from "../services/chatService";
import { isUserInConversation } from "./presence";
import { notify } from "../services/notificationService";
import UserModel from "../models/UserModel";

type AuthedSocket = Socket & { data: { userId: string } };

function userRoom(userId: string): string {
  return `user:${userId}`;
}

function conversationRoom(conversationId: string): string {
  return `conversation:${conversationId}`;
}

export function registerChatSocketHandlers(io: Server, socket: AuthedSocket) {
  const userId = socket.data.userId;
  socket.join(userRoom(userId));

  socket.on("join_conversation", async (payload: unknown) => {
    try {
      const conversationId =
        typeof payload === "object" &&
        payload !== null &&
        typeof (payload as { conversationId?: unknown }).conversationId ===
          "string"
          ? (payload as { conversationId: string }).conversationId.trim()
          : "";
      if (!conversationId) {
        socket.emit("message_error", {
          code: "INVALID_PAYLOAD",
          message: "conversationId is required",
        });
        return;
      }

      await assertConversationParticipant(conversationId, userId);
      await markConversationRead(conversationId, userId);
      await socket.join(conversationRoom(conversationId));
      socket.emit("joined_conversation", { conversationId, unreadCount: 0 });
      io.to(userRoom(userId)).emit("conversation_updated", {
        conversationId,
        unreadCount: 0,
        senderId: userId,
      });
    } catch (err) {
      const code = err instanceof Error ? err.message : "ERROR";
      socket.emit("message_error", {
        code,
        message:
          code === "FORBIDDEN"
            ? "You are not a participant in this conversation"
            : code === "NOT_FOUND"
              ? "Conversation not found"
              : "Failed to join conversation",
      });
    }
  });

  socket.on("leave_conversation", async (payload: unknown) => {
    const conversationId =
      typeof payload === "object" &&
      payload !== null &&
      typeof (payload as { conversationId?: unknown }).conversationId ===
        "string"
        ? (payload as { conversationId: string }).conversationId.trim()
        : "";
    if (!conversationId) return;
    await socket.leave(conversationRoom(conversationId));
  });

  socket.on("send_message", async (payload: unknown) => {
    try {
      const body =
        typeof payload === "object" && payload !== null
          ? (payload as {
              conversationId?: unknown;
              text?: unknown;
              media?: unknown;
            })
          : {};
      const conversationId =
        typeof body.conversationId === "string"
          ? body.conversationId.trim()
          : "";
      const text = typeof body.text === "string" ? body.text : "";
      const media = body.media;

      if (!conversationId) {
        socket.emit("message_error", {
          code: "INVALID_PAYLOAD",
          message: "conversationId is required",
        });
        return;
      }

      const hasMedia = Array.isArray(media) && media.length > 0;
      if (!text.trim() && !hasMedia) {
        socket.emit("message_error", {
          code: "INVALID_PAYLOAD",
          message: "Provide text and/or media",
        });
        return;
      }

      const saved = await persistMessage(conversationId, userId, {
        text,
        media,
      });
      const {
        otherUserId,
        lastMessagePreview,
        recipientUnreadCount,
        ...message
      } = saved as {
        otherUserId: string;
        lastMessagePreview: string;
        recipientUnreadCount: number;
        _id: unknown;
        conversationId: unknown;
        senderId: unknown;
        text: string;
        media: unknown[];
        createdAt: Date;
      };

      const eventPayload = {
        _id: message._id,
        conversationId: message.conversationId,
        senderId: message.senderId,
        text: message.text,
        media: message.media,
        createdAt: message.createdAt,
      };

      io.to(conversationRoom(conversationId)).emit("new_message", eventPayload);
      io.to(userRoom(userId)).emit("new_message", eventPayload);
      io.to(userRoom(otherUserId)).emit("new_message", eventPayload);

      // Sender: unread stays 0 for this thread
      io.to(userRoom(userId)).emit("conversation_updated", {
        conversationId,
        lastMessageAt: message.createdAt,
        lastMessagePreview,
        senderId: userId,
        unreadCount: 0,
      });
      // Recipient: include unreadCount so list badges update live
      io.to(userRoom(otherUserId)).emit("conversation_updated", {
        conversationId,
        lastMessageAt: message.createdAt,
        lastMessagePreview,
        senderId: userId,
        unreadCount: recipientUnreadCount,
      });

      // Inbox always; FCM only if recipient is not viewing this conversation
      const inConversation = await isUserInConversation(
        otherUserId,
        conversationId,
      );
      const sender = await UserModel.findById(userId)
        .select("fullName email")
        .lean();
      const senderName = (sender?.fullName || sender?.email || "Someone").trim();
      const preview =
        typeof lastMessagePreview === "string" && lastMessagePreview.trim()
          ? lastMessagePreview.trim().slice(0, 120)
          : "Sent you a message";
      void notify({
        userId: otherUserId,
        type: "chat",
        eventType: "message",
        title: senderName,
        body: preview,
        data: {
          conversationId,
          messageId: String(message._id),
          senderId: userId,
        },
        push: !inConversation,
      });
    } catch (err) {
      const code = err instanceof Error ? err.message : "ERROR";
      const messages: Record<string, string> = {
        FORBIDDEN: "You are not a participant in this conversation",
        NOT_FOUND: "Conversation not found",
        NOT_CONNECTED: "You can only chat with your connections",
        BLOCKED: "You cannot message this user",
        MESSAGE_EMPTY: "Provide text and/or media",
        MESSAGE_TOO_LONG: "Message is too long",
        MEDIA_TOO_MANY: "Maximum 10 media items per message",
        INVALID_MEDIA: "Invalid media item (url required)",
      };
      socket.emit("message_error", {
        code,
        message: messages[code] ?? "Failed to send message",
      });
    }
  });

  socket.on("typing_start", async (payload: unknown) => {
    await relayTyping(io, socket, userId, payload, true);
  });

  socket.on("typing_stop", async (payload: unknown) => {
    await relayTyping(io, socket, userId, payload, false);
  });
}

async function relayTyping(
  io: Server,
  socket: AuthedSocket,
  userId: string,
  payload: unknown,
  isTyping: boolean,
) {
  try {
    const conversationId =
      typeof payload === "object" &&
      payload !== null &&
      typeof (payload as { conversationId?: unknown }).conversationId ===
        "string"
        ? (payload as { conversationId: string }).conversationId.trim()
        : "";
    if (!conversationId) return;

    const { otherUserId } = await assertConversationParticipant(
      conversationId,
      userId,
    );
    io.to(userRoom(otherUserId)).emit("typing", {
      conversationId,
      userId,
      isTyping,
    });
    socket
      .to(conversationRoom(conversationId))
      .emit("typing", { conversationId, userId, isTyping });
  } catch {
    /* ignore typing auth failures */
  }
}
