import { Types } from "mongoose";
import ConversationModel from "../models/ConversationModel";
import MessageModel from "../models/MessageModel";
import UserModel from "../models/UserModel";
import { isConnected, PaginatedResult } from "./connectionService";
import { assertNotBlocked } from "./blockService";
import { buildImageUrl } from "../utils/imageUrl";
import {
  ChatMediaType,
  IChatMediaItem,
} from "../interfaces/models/messageInterface";
import { uploadMulterFileToR2 } from "./r2StorageService";

export type ChatMediaInput = {
  url: string;
  type: ChatMediaType;
  mimeType?: string | null;
  fileName?: string | null;
};

function getUnreadCount(
  unreadCounts: Map<string, number> | Record<string, number> | undefined | null,
  userId: string,
): number {
  if (!unreadCounts) return 0;
  if (unreadCounts instanceof Map) {
    return Math.max(0, Number(unreadCounts.get(userId) ?? 0));
  }
  return Math.max(
    0,
    Number((unreadCounts as Record<string, number>)[userId] ?? 0),
  );
}

function sortedParticipantIds(
  a: string,
  b: string,
): [Types.ObjectId, Types.ObjectId] {
  const sorted = [a, b].sort();
  return [new Types.ObjectId(sorted[0]), new Types.ObjectId(sorted[1])];
}

function mapUserSummary(
  user: {
    _id: unknown;
    fullName?: string;
    email?: string;
    image?: string | null;
  } | null,
) {
  if (!user) return null;
  return {
    _id: user._id,
    fullName: user.fullName ?? "",
    email: user.email ?? "",
    image: user.image ? buildImageUrl(user.image) : null,
  };
}

function normalizeStoredMediaUrl(url: string): string {
  const trimmed = url.trim();
  if (!trimmed) return "";
  if (/^https?:\/\//i.test(trimmed)) return trimmed;
  const bare = trimmed.replace(/^\/+/, "").replace(/^public\//i, "");
  if (bare.toLowerCase().startsWith("uploads/")) {
    return `/${bare}`;
  }
  return `/uploads/${bare.replace(/^uploads\//i, "")}`;
}

function mediaTypeFromMime(mime: string, fileName: string): ChatMediaType {
  if (mime.startsWith("video/") || /\.(mp4|mov|webm)$/i.test(fileName)) {
    return "video";
  }
  if (
    mime.startsWith("image/") ||
    /\.(jpe?g|png|gif|webp|jfif)$/i.test(fileName)
  ) {
    return "image";
  }
  return "file";
}

function mapMediaForResponse(items: IChatMediaItem[] | undefined) {
  return (items ?? []).map((m) => ({
    url: buildImageUrl(m.url) ?? m.url,
    type: m.type,
    mimeType: m.mimeType ?? null,
    fileName: m.fileName ?? null,
  }));
}

export function mapMessage(msg: {
  _id: unknown;
  conversationId: unknown;
  senderId: unknown;
  text?: string | null;
  media?: IChatMediaItem[];
  createdAt: Date;
}) {
  return {
    _id: msg._id,
    conversationId: msg.conversationId,
    senderId: msg.senderId,
    text: msg.text ?? "",
    media: mapMediaForResponse(msg.media),
    createdAt: msg.createdAt,
  };
}

function buildPreview(text: string, media: ChatMediaInput[]): string {
  if (text.trim()) {
    const t = text.trim();
    return t.length > 200 ? `${t.slice(0, 197)}...` : t;
  }
  if (media.length === 0) return "";
  const first = media[0];
  if (first.type === "image") {
    return media.length > 1 ? `📷 ${media.length} photos` : "📷 Photo";
  }
  if (first.type === "video") {
    return media.length > 1 ? `🎬 ${media.length} videos` : "🎬 Video";
  }
  return media.length > 1 ? `📎 ${media.length} files` : "📎 Attachment";
}

function normalizeMediaInput(raw: unknown): ChatMediaInput[] {
  if (!Array.isArray(raw) || raw.length === 0) return [];
  if (raw.length > 10) throw new Error("MEDIA_TOO_MANY");

  return raw.map((item) => {
    if (!item || typeof item !== "object") throw new Error("INVALID_MEDIA");
    const o = item as Record<string, unknown>;
    const url = typeof o.url === "string" ? normalizeStoredMediaUrl(o.url) : "";
    if (!url) throw new Error("INVALID_MEDIA");

    let type: ChatMediaType =
      o.type === "image" || o.type === "video" || o.type === "file"
        ? o.type
        : "file";
    const mimeType = typeof o.mimeType === "string" ? o.mimeType.trim() : null;
    const fileName = typeof o.fileName === "string" ? o.fileName.trim() : null;
    if (o.type == null && mimeType) {
      type = mediaTypeFromMime(mimeType, fileName ?? url);
    }
    return { url, type, mimeType, fileName };
  });
}

export async function assertConversationParticipant(
  conversationId: string,
  userId: string,
): Promise<{
  conversation: { _id: Types.ObjectId; participants: Types.ObjectId[] };
  otherUserId: string;
}> {
  const conversation = await ConversationModel.findById(conversationId).lean();
  if (!conversation) throw new Error("NOT_FOUND");

  const participantIds = conversation.participants.map(String);
  if (!participantIds.includes(userId)) throw new Error("FORBIDDEN");

  const otherUserId = participantIds.find((id) => id !== userId)!;
  return {
    conversation: {
      _id: conversation._id as Types.ObjectId,
      participants: conversation.participants,
    },
    otherUserId,
  };
}

export async function getOrCreateConversation(
  userId: string,
  otherUserId: string,
): Promise<Record<string, unknown>> {
  if (userId === otherUserId) throw new Error("CANNOT_SELF_CHAT");

  await assertNotBlocked(userId, otherUserId);

  const connected = await isConnected(userId, otherUserId);
  if (!connected) throw new Error("NOT_CONNECTED");

  const other = await UserModel.findOne({
    _id: new Types.ObjectId(otherUserId),
    isDeleted: false,
    isBanned: false,
  })
    .select("_id fullName email image")
    .lean();
  if (!other) throw new Error("USER_NOT_FOUND");

  const participants = sortedParticipantIds(userId, otherUserId);

  let conversation = await ConversationModel.findOne({
    "participants.0": participants[0],
    "participants.1": participants[1],
  });

  if (!conversation) {
    conversation = await ConversationModel.create({
      participants,
      lastMessageAt: null,
      lastMessagePreview: null,
    });
  }

  return {
    _id: conversation._id,
    participants: conversation.participants,
    otherUser: mapUserSummary(other),
    lastMessageAt: conversation.lastMessageAt,
    lastMessagePreview: conversation.lastMessagePreview,
    unreadCount: getUnreadCount(conversation.unreadCounts, userId),
    createdAt: conversation.createdAt,
  };
}

export async function listConversations(
  userId: string,
  page: number,
  limit: number,
): Promise<PaginatedResult<Record<string, unknown>>> {
  const skip = (page - 1) * limit;
  const uid = new Types.ObjectId(userId);

  const query = { participants: uid };
  const total = await ConversationModel.countDocuments(query);
  const conversations = await ConversationModel.find(query)
    .sort({ lastMessageAt: -1, updatedAt: -1 })
    .skip(skip)
    .limit(limit)
    .lean();

  if (conversations.length === 0) {
    return { data: [], total, page, totalPages: Math.ceil(total / limit) || 0 };
  }

  const otherIds = conversations.map((c) => {
    const ids = c.participants.map(String);
    return new Types.ObjectId(ids.find((id) => id !== userId)!);
  });

  const users = await UserModel.find({
    _id: { $in: otherIds },
    isDeleted: false,
  })
    .select("_id fullName email image")
    .lean();
  const userMap = new Map(users.map((u) => [String(u._id), u]));

  const data = conversations.map((c) => {
    const otherId = c.participants.map(String).find((id) => id !== userId)!;
    return {
      _id: c._id,
      participants: c.participants,
      otherUser: mapUserSummary(userMap.get(otherId) ?? null),
      lastMessageAt: c.lastMessageAt,
      lastMessagePreview: c.lastMessagePreview,
      unreadCount: getUnreadCount(c.unreadCounts, userId),
      createdAt: c.createdAt,
      updatedAt: c.updatedAt,
    };
  });

  return {
    data,
    total,
    page,
    totalPages: Math.ceil(total / limit) || 0,
  };
}

export async function listMessages(
  conversationId: string,
  userId: string,
  page: number,
  limit: number,
  before?: string,
): Promise<PaginatedResult<Record<string, unknown>>> {
  await assertConversationParticipant(conversationId, userId);

  // Opening the thread marks messages as read for this user.
  await markConversationRead(conversationId, userId);

  const query: Record<string, unknown> = {
    conversationId: new Types.ObjectId(conversationId),
  };
  if (before) {
    query._id = { $lt: new Types.ObjectId(before) };
  }

  const total = await MessageModel.countDocuments({
    conversationId: new Types.ObjectId(conversationId),
  });

  const skip = before ? 0 : (page - 1) * limit;
  const messages = await MessageModel.find(query)
    .sort({ createdAt: -1, _id: -1 })
    .skip(skip)
    .limit(limit)
    .lean();

  return {
    data: messages.map(mapMessage).reverse(),
    total,
    page: before ? 1 : page,
    totalPages: Math.ceil(total / limit) || 0,
  };
}

/**
 * Reset unread count for a participant (e.g. when they open the chat).
 */
export async function markConversationRead(
  conversationId: string,
  userId: string,
): Promise<{ unreadCount: number }> {
  await assertConversationParticipant(conversationId, userId);

  await ConversationModel.updateOne(
    { _id: new Types.ObjectId(conversationId) },
    { $set: { [`unreadCounts.${userId}`]: 0 } },
  );

  return { unreadCount: 0 };
}

export async function persistMessage(
  conversationId: string,
  senderId: string,
  opts: { text?: string; media?: unknown },
): Promise<Record<string, unknown>> {
  const trimmed = typeof opts.text === "string" ? opts.text.trim() : "";
  if (trimmed.length > 2000) throw new Error("MESSAGE_TOO_LONG");

  const media = normalizeMediaInput(opts.media);
  if (!trimmed && media.length === 0) {
    throw new Error("MESSAGE_EMPTY");
  }

  const { conversation, otherUserId } = await assertConversationParticipant(
    conversationId,
    senderId,
  );

  await assertNotBlocked(senderId, otherUserId);

  const stillConnected = await isConnected(senderId, otherUserId);
  if (!stillConnected) throw new Error("NOT_CONNECTED");

  const message = await MessageModel.create({
    conversationId: conversation._id,
    senderId: new Types.ObjectId(senderId),
    text: trimmed,
    media,
  });

  const preview = buildPreview(trimmed, media);

  // Bump recipient unread; clear sender's unread for this thread.
  const updated = await ConversationModel.findOneAndUpdate(
    { _id: conversation._id },
    {
      $set: {
        lastMessageAt: message.createdAt,
        lastMessagePreview: preview,
        [`unreadCounts.${senderId}`]: 0,
      },
      $inc: {
        [`unreadCounts.${otherUserId}`]: 1,
      },
    },
    { new: true },
  );

  const recipientUnread = getUnreadCount(updated?.unreadCounts, otherUserId);

  return {
    ...mapMessage(message),
    otherUserId,
    lastMessagePreview: preview,
    recipientUnreadCount: recipientUnread,
  };
}

export async function buildChatMediaFromUploads(
  files: Express.Multer.File[],
): Promise<ChatMediaInput[]> {
  return Promise.all(
    files.map(async (f) => {
      const { url } = await uploadMulterFileToR2(f, "chat");
      return {
        url,
        type: mediaTypeFromMime(f.mimetype, f.originalname),
        mimeType: f.mimetype,
        fileName: f.originalname,
      };
    }),
  );
}
