import { Types } from "mongoose";
import DeviceModel from "../models/DevicesModel";
import NotificationModel from "../models/NotificationModel";
import {
  NotificationEventType,
  NotificationType,
} from "../interfaces/models/notificationInterface";
import { getFirebaseMessaging } from "../config/firebase";

export type NotifyInput = {
  userId: string;
  type: NotificationType;
  eventType: NotificationEventType;
  title: string;
  body: string;
  data?: Record<string, string | number | boolean | null | undefined>;
  /** When false, only inbox row is created (no FCM). Default true. */
  push?: boolean;
  /** Unique key — duplicate notify is ignored (inbox + push). */
  dedupeKey?: string;
};

function toStringData(
  data: Record<string, string | number | boolean | null | undefined> = {},
): Record<string, string> {
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(data)) {
    if (v === undefined || v === null) continue;
    out[k] = String(v);
  }
  return out;
}

function mapToObject(
  data: Map<string, string> | Record<string, string> | undefined,
): Record<string, string> {
  if (!data) return {};
  if (data instanceof Map) {
    return Object.fromEntries(data.entries());
  }
  return { ...data };
}

export function mapNotification(doc: {
  _id: unknown;
  type: string;
  eventType: string;
  title: string;
  body: string;
  data?: Map<string, string> | Record<string, string>;
  isRead: boolean;
  readAt?: Date | null;
  createdAt: Date;
}): Record<string, unknown> {
  return {
    _id: doc._id,
    type: doc.type,
    eventType: doc.eventType,
    title: doc.title,
    body: doc.body,
    data: mapToObject(doc.data),
    isRead: doc.isRead,
    readAt: doc.readAt ?? null,
    createdAt: doc.createdAt,
  };
}

/**
 * Create inbox row + optional FCM push. Never throws to callers — logs failures.
 */
export async function notify(input: NotifyInput): Promise<void> {
  try {
    if (!Types.ObjectId.isValid(input.userId)) return;

    const stringData = toStringData({
      type: input.type,
      eventType: input.eventType,
      ...input.data,
    });

    let created;
    try {
      const payload: Record<string, unknown> = {
        userId: new Types.ObjectId(input.userId),
        type: input.type,
        eventType: input.eventType,
        title: input.title.slice(0, 200),
        body: input.body.slice(0, 500),
        data: stringData,
        isRead: false,
      };
      // Only set when present — never store null (breaks sparse unique index)
      if (input.dedupeKey) {
        payload.dedupeKey = input.dedupeKey;
      }
      created = await NotificationModel.create(payload);
    } catch (err: unknown) {
      const code = (err as { code?: number })?.code;
      if (code === 11000 && input.dedupeKey) {
        return; // duplicate payment / settle — skip
      }
      throw err;
    }

    if (input.push === false) return;

    await sendPushToUser(input.userId, {
      title: created.title,
      body: created.body,
      data: {
        ...stringData,
        notificationId: String(created._id),
      },
    });
  } catch (err) {
    console.error("[notification] notify failed", err);
  }
}

async function sendPushToUser(
  userId: string,
  payload: { title: string; body: string; data: Record<string, string> },
): Promise<void> {
  const messaging = getFirebaseMessaging();
  if (!messaging) {
    console.warn(
      `[notification] FCM skipped userId=${userId.slice(0, 8)}… — Firebase not initialized`,
    );
    return;
  }

  const devices = await DeviceModel.find({
    userId: new Types.ObjectId(userId),
    status: true,
    deviceToken: { $nin: [null, ""] },
  })
    .select("deviceToken")
    .lean();

  const tokens = [
    ...new Set(
      devices
        .map((d) => (d.deviceToken || "").trim())
        .filter((t) => t.length > 0),
    ),
  ];
  if (tokens.length === 0) {
    console.warn(
      `[notification] FCM skipped userId=${userId.slice(0, 8)}… — no active deviceToken (login with FCM token)`,
    );
    return;
  }

  // FCM data values must be strings (already ensured).
  const response = await messaging.sendEachForMulticast({
    tokens,
    notification: {
      title: payload.title,
      body: payload.body,
    },
    data: payload.data,
    android: {
      priority: "high",
    },
    apns: {
      payload: {
        aps: {
          sound: "default",
        },
      },
    },
  });

  console.log(
    `[notification] FCM userId=${userId.slice(0, 8)}… tokens=${tokens.length} success=${response.successCount} failure=${response.failureCount}`,
  );

  const invalidTokens: string[] = [];
  response.responses.forEach(
    (
      res: { success: boolean; error?: { code?: string; message?: string } },
      idx: number,
    ) => {
      if (res.success) return;
      const code = res.error?.code ?? "";
      if (
        code === "messaging/registration-token-not-registered" ||
        code === "messaging/invalid-registration-token"
      ) {
        invalidTokens.push(tokens[idx]);
      } else {
        const msg = res.error?.message ?? "";
        console.warn(
          `[notification] FCM send failed token=${tokens[idx]?.slice(0, 12)}…`,
          code,
          msg,
        );
        if (
          code === "messaging/mismatched-credential" ||
          msg.includes("cloudmessaging.messages.create")
        ) {
          console.error(
            "[notification] FCM IAM FIX REQUIRED: grant the Firebase Admin SDK service account " +
              "role \"Firebase Cloud Messaging API Admin\" (or Firebase Admin) on project " +
              "tanktrack-client-prod, and enable Firebase Cloud Messaging API. " +
              "See docs/push-notifications-README.md § FCM IAM",
          );
        }
      }
    },
  );

  if (invalidTokens.length > 0) {
    console.warn(
      `[notification] Deactivating ${invalidTokens.length} invalid FCM token(s)`,
    );
    await DeviceModel.updateMany(
      { deviceToken: { $in: invalidTokens } },
      { $set: { status: false } },
    );
  }
}

export async function listNotifications(
  userId: string,
  page: number,
  limit: number,
): Promise<{
  data: Record<string, unknown>[];
  total: number;
  page: number;
  totalPages: number;
  unreadCount: number;
}> {
  const filter = { userId: new Types.ObjectId(userId) };
  const skip = (page - 1) * limit;

  const [rows, total, unreadCount] = await Promise.all([
    NotificationModel.find(filter)
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .lean(),
    NotificationModel.countDocuments(filter),
    NotificationModel.countDocuments({ ...filter, isRead: false }),
  ]);

  return {
    data: rows.map((r) => mapNotification(r as never)),
    total,
    page,
    totalPages: Math.ceil(total / limit) || 1,
    unreadCount,
  };
}

export async function markNotificationRead(
  userId: string,
  notificationId: string,
): Promise<Record<string, unknown>> {
  if (!Types.ObjectId.isValid(notificationId)) {
    throw new Error("NOT_FOUND");
  }

  const updated = await NotificationModel.findOneAndUpdate(
    {
      _id: new Types.ObjectId(notificationId),
      userId: new Types.ObjectId(userId),
    },
    { $set: { isRead: true, readAt: new Date() } },
    { new: true },
  ).lean();

  if (!updated) throw new Error("NOT_FOUND");
  return mapNotification(updated as never);
}

export async function markAllNotificationsRead(
  userId: string,
): Promise<{ modifiedCount: number }> {
  const result = await NotificationModel.updateMany(
    { userId: new Types.ObjectId(userId), isRead: false },
    { $set: { isRead: true, readAt: new Date() } },
  );
  return { modifiedCount: result.modifiedCount };
}
