import { Types, PipelineStage } from "mongoose";
import ConnectionModel from "../models/ConnectionModel";
import UserModel from "../models/UserModel";
import { sendEmail } from "../utils/SendEmail";
import { buildImageUrl } from "../utils/imageUrl";
import { notify } from "./notificationService";

async function displayName(userId: string): Promise<string> {
  const u = await UserModel.findById(userId).select("fullName email").lean();
  return (u?.fullName || u?.email || "Someone").trim();
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  totalPages: number;
}

export type NearbyConnectionStatus =
  | "none"
  | "pending_sent"
  | "pending_received"
  | "connected";

const ACTIVE_USER_FILTER = { isDeleted: false, isBanned: false };

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase();
}

function escapeRegex(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function findUserByEmail(email: string) {
  const normalized = normalizeEmail(email);
  const user = await UserModel.findOne({
    email: { $regex: new RegExp(`^${escapeRegex(normalized)}$`, "i") },
    isDeleted: false,
  }).lean();

  if (user?.isBanned) throw new Error("USER_BANNED");
  if (!user) return null;

  return user;
}

export async function sendInviteOrRequest(
  senderId: string,
  email: string,
): Promise<{
  type: "request_sent" | "invite_sent";
  requestId: string;
}> {
  const sender = await UserModel.findById(senderId).lean();
  if (!sender) throw new Error("SENDER_NOT_FOUND");
  if (sender.isBanned) throw new Error("USER_BANNED");

  const normalizedEmail = normalizeEmail(email);
  const target = await findUserByEmail(normalizedEmail);

  if (!target) {
    const existingEmailInvite = await ConnectionModel.findOne({
      senderId: new Types.ObjectId(senderId),
      inviteeEmail: normalizedEmail,
      status: "pending",
    }).lean();

    if (existingEmailInvite) {
      throw new Error("REQUEST_ALREADY_PENDING");
    }

    const invite = await ConnectionModel.create({
      senderId: new Types.ObjectId(senderId),
      inviteeEmail: normalizedEmail,
      status: "pending",
    });

    await sendEmail(
      normalizedEmail,
      "You've been invited to Tank Track",
      inviteEmailTemplate(sender.fullName || sender.email),
    );

    return { type: "invite_sent", requestId: String(invite._id) };
  }

  const receiverId = String(target._id);

  if (senderId === receiverId) {
    throw new Error("CANNOT_SELF_CONNECT");
  }

  const existing = await ConnectionModel.findOne({
    $or: [
      {
        senderId: new Types.ObjectId(senderId),
        receiverId: new Types.ObjectId(receiverId),
      },
      {
        senderId: new Types.ObjectId(receiverId),
        receiverId: new Types.ObjectId(senderId),
      },
    ],
    status: { $in: ["pending", "accepted"] },
  }).lean();

  if (existing) {
    if (existing.status === "accepted") throw new Error("ALREADY_CONNECTED");
    throw new Error("REQUEST_ALREADY_PENDING");
  }

  const connection = await ConnectionModel.create({
    senderId: new Types.ObjectId(senderId),
    receiverId: new Types.ObjectId(receiverId),
    status: "pending",
  });

  const senderName = await displayName(senderId);
  void notify({
    userId: receiverId,
    type: "connection",
    eventType: "request_received",
    title: "New connection request",
    body: `${senderName} sent you a connection request`,
    data: {
      connectionId: String(connection._id),
      senderId,
      receiverId,
    },
  });

  return { type: "request_sent", requestId: String(connection._id) };
}

export async function getNearbyUsers(
  userId: string,
  latitude: number,
  longitude: number,
  radiusKm: number,
  page: number,
  limit: number,
): Promise<PaginatedResult<Record<string, unknown>>> {
  const skip = (page - 1) * limit;

  const basePipeline: PipelineStage[] = [
    {
      $geoNear: {
        near: { type: "Point", coordinates: [longitude, latitude] },
        distanceField: "distanceMeters",
        maxDistance: radiusKm * 1000,
        spherical: true,
        query: {
          _id: { $ne: new Types.ObjectId(userId) },
          ...ACTIVE_USER_FILTER,
          isVerified: true,
          "location.coordinates": { $exists: true, $ne: [] },
        },
      },
    },
    {
      $project: {
        _id: 1,
        fullName: 1,
        email: 1,
        image: 1,
        location: 1,
        distanceMeters: 1,
      },
    },
  ];

  const [countResult] = await UserModel.aggregate([
    ...basePipeline,
    { $count: "total" } as PipelineStage,
  ]);
  const total: number = countResult?.total ?? 0;

  const users = await UserModel.aggregate([
    ...basePipeline,
    { $skip: skip } as PipelineStage,
    { $limit: limit } as PipelineStage,
  ]);

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

  const nearbyUserIds = users.map((u: { _id: Types.ObjectId }) => u._id);
  const connections = await ConnectionModel.find({
    $or: [
      { senderId: new Types.ObjectId(userId), receiverId: { $in: nearbyUserIds } },
      { receiverId: new Types.ObjectId(userId), senderId: { $in: nearbyUserIds } },
    ],
    status: { $in: ["pending", "accepted"] },
  }).lean();

  const connectionMap = new Map<string, { status: string; senderId: string }>();
  for (const conn of connections) {
    const otherId =
      String(conn.senderId) === userId
        ? String(conn.receiverId)
        : String(conn.senderId);
    connectionMap.set(otherId, {
      status: conn.status,
      senderId: String(conn.senderId),
    });
  }

  const data = users.map((u: Record<string, unknown>) => {
    const entry = connectionMap.get(String(u._id));
    let connectionStatus: NearbyConnectionStatus = "none";

    if (entry) {
      if (entry.status === "accepted") {
        connectionStatus = "connected";
      } else if (entry.status === "pending") {
        connectionStatus =
          entry.senderId === userId ? "pending_sent" : "pending_received";
      }
    }

    return {
      _id: u._id,
      fullName: u.fullName,
      email: u.email,
      image: u.image ? buildImageUrl(String(u.image)) : null,
      distanceKm: Number((Number(u.distanceMeters) / 1000).toFixed(2)),
      connectionStatus,
    };
  });

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

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

  const connections = await ConnectionModel.find({
    $or: [{ senderId: uid }, { receiverId: uid }],
    status: "accepted",
  })
    .select("senderId receiverId updatedAt")
    .lean();

  if (connections.length === 0) {
    return { data: [], total: 0, page, totalPages: 0 };
  }

  const friendIds = connections.map((c) =>
    String(c.senderId) === userId ? c.receiverId : c.senderId,
  );

  const userQuery: Record<string, unknown> = {
    _id: { $in: friendIds },
    ...ACTIVE_USER_FILTER,
  };
  if (search && search.trim()) {
    const esc = escapeRegex(search.trim());
    userQuery.$or = [
      { fullName: new RegExp(esc, "i") },
      { email: new RegExp(esc, "i") },
    ];
  }

  const total = await UserModel.countDocuments(userQuery);
  const users = await UserModel.find(userQuery)
    .select("_id fullName email image")
    .skip(skip)
    .limit(limit)
    .lean();

  const connectedAtMap = new Map<string, Date>();
  for (const c of connections) {
    const friendId =
      String(c.senderId) === userId ? String(c.receiverId) : String(c.senderId);
    connectedAtMap.set(friendId, c.updatedAt as Date);
  }

  const data = users.map((u) => ({
    _id: u._id,
    fullName: u.fullName,
    email: u.email,
    image: u.image ? buildImageUrl(u.image) : null,
    connectedAt: connectedAtMap.get(String(u._id)) ?? null,
  }));

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

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

  const query =
    type === "received"
      ? { receiverId: uid, status: "pending" }
      : { senderId: uid, status: "pending" };

  const total = await ConnectionModel.countDocuments(query);
  const requests = await ConnectionModel.find(query)
    .sort({ createdAt: -1 })
    .skip(skip)
    .limit(limit)
    .lean();

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

  const otherIds = requests
    .map((r) => (type === "received" ? r.senderId : r.receiverId))
    .filter((id): id is Types.ObjectId => id != null);

  const users =
    otherIds.length > 0
      ? await UserModel.find({ _id: { $in: otherIds }, ...ACTIVE_USER_FILTER })
          .select("_id fullName email image")
          .lean()
      : [];

  const userMap = new Map(users.map((u) => [String(u._id), u]));

  const data = requests.map((r) => {
    if (type === "sent" && r.inviteeEmail && !r.receiverId) {
      return {
        requestId: r._id,
        inviteType: "email",
        inviteeEmail: r.inviteeEmail,
        user: {
          _id: null,
          fullName: null,
          email: r.inviteeEmail,
          image: null,
        },
        requestedAt: r.createdAt,
      };
    }

    const otherId = String(type === "received" ? r.senderId : r.receiverId);
    const user = userMap.get(otherId);
    return {
      requestId: r._id,
      inviteType: "user",
      user: user
        ? {
            _id: user._id,
            fullName: user.fullName,
            email: user.email,
            image: user.image ? buildImageUrl(user.image) : null,
          }
        : null,
      requestedAt: r.createdAt,
    };
  });

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

export async function acceptConnectionRequest(
  requestId: string,
  userId: string,
): Promise<void> {
  const request = await ConnectionModel.findById(requestId);
  if (!request) throw new Error("REQUEST_NOT_FOUND");
  if (!request.receiverId) throw new Error("REQUEST_NOT_FOUND");
  if (String(request.receiverId) !== userId) throw new Error("FORBIDDEN");
  if (request.status !== "pending") throw new Error("INVALID_STATUS");

  request.status = "accepted";
  await request.save();

  const accepterName = await displayName(userId);
  void notify({
    userId: String(request.senderId),
    type: "connection",
    eventType: "accepted",
    title: "Connection accepted",
    body: `${accepterName} accepted your connection request`,
    data: {
      connectionId: String(request._id),
      senderId: String(request.senderId),
      receiverId: userId,
    },
  });
}

export async function rejectConnectionRequest(
  requestId: string,
  userId: string,
): Promise<void> {
  const request = await ConnectionModel.findById(requestId);
  if (!request) throw new Error("REQUEST_NOT_FOUND");
  if (!request.receiverId) throw new Error("REQUEST_NOT_FOUND");
  if (String(request.receiverId) !== userId) throw new Error("FORBIDDEN");
  if (request.status !== "pending") throw new Error("INVALID_STATUS");

  request.status = "rejected";
  await request.save();
}

export async function cancelConnectionRequest(
  requestId: string,
  userId: string,
): Promise<void> {
  const request = await ConnectionModel.findById(requestId);
  if (!request) throw new Error("REQUEST_NOT_FOUND");
  if (String(request.senderId) !== userId) throw new Error("FORBIDDEN");
  if (request.status !== "pending") throw new Error("INVALID_STATUS");

  request.status = "cancelled";
  await request.save();
}

export async function isConnected(
  userIdA: string,
  userIdB: string,
): Promise<boolean> {
  const connection = await ConnectionModel.findOne({
    status: "accepted",
    $or: [
      {
        senderId: new Types.ObjectId(userIdA),
        receiverId: new Types.ObjectId(userIdB),
      },
      {
        senderId: new Types.ObjectId(userIdB),
        receiverId: new Types.ObjectId(userIdA),
      },
    ],
  })
    .select("_id")
    .lean();
  return connection !== null;
}

export async function removeConnection(
  targetUserId: string,
  userId: string,
): Promise<void> {
  if (targetUserId === userId) {
    throw new Error("CANNOT_SELF_CONNECT");
  }

  const connection = await ConnectionModel.findOne({
    status: "accepted",
    $or: [
      {
        senderId: new Types.ObjectId(userId),
        receiverId: new Types.ObjectId(targetUserId),
      },
      {
        senderId: new Types.ObjectId(targetUserId),
        receiverId: new Types.ObjectId(userId),
      },
    ],
  });

  if (!connection) throw new Error("CONNECTION_NOT_FOUND");

  await connection.deleteOne();
}

function inviteEmailTemplate(senderName: string): string {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>You've been invited to Tank Track</title>
    </head>
    <body style="font-family: 'Segoe UI', sans-serif; background: #001f3f; margin: 0; padding: 0; color: #ffffff;">
      <div style="max-width: 600px; margin: 50px auto; background: #0e1a2b; border-radius: 12px; overflow: hidden;">
        <div style="background-color: #003B73; padding: 30px; text-align: center;">
          <h2 style="margin: 0; color: #ffffff;">You're Invited to Tank Track</h2>
        </div>
        <div style="padding: 30px; text-align: center;">
          <p style="font-size: 16px; color: #cccccc;">
            <strong>${senderName}</strong> has invited you to join Tank Track.
          </p>
          <p style="font-size: 14px; color: #cccccc;">
            Download the app and sign up to connect with your friends.
          </p>
        </div>
      </div>
    </body>
    </html>
  `;
}
