/**
 * Apple-Style Device-Approval (iCloud Sign-In Pattern).
 *
 * Wenn ein neues Gerät registriert wird und das Device-Limit erreicht ist,
 * kann der User auf einem bereits angemeldeten Gerät den Login bestätigen
 * (Variante 2 — Code wird auf BEIDEN Geräten gezeigt für visuellen Vergleich).
 *
 * Fallback: Wenn kein anderes Device aktiv → Email mit One-Click-Approval-Link.
 *
 * TTL: 10 Minuten. Codes sind 6-stellig numerisch.
 */

import crypto from "node:crypto";
import { usePrisma } from "../utils/prisma";

const APPROVAL_TTL_MS = 10 * 60 * 1000; // 10 min
/** Devices die innerhalb dieser Zeit gesehen wurden gelten als "online genug für Push". */
const ACTIVE_DEVICE_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7d

export interface DeviceApprovalRecord {
  id: string;
  userId: string;
  newDeviceId: string;
  newPlatform: string;
  newModel: string | null;
  newName: string | null;
  newOsVersion: string | null;
  code: string;
  status: string;
  approvedByDeviceRowId: string | null;
  approvedAt: Date | null;
  rejectedAt: Date | null;
  evictedDeviceRowId: string | null;
  emailSentAt: Date | null;
  createdAt: Date;
  expiresAt: Date;
}

const APPROVAL_SELECT = {
  id: true,
  userId: true,
  newDeviceId: true,
  newPlatform: true,
  newModel: true,
  newName: true,
  newOsVersion: true,
  code: true,
  status: true,
  approvedByDeviceRowId: true,
  approvedAt: true,
  rejectedAt: true,
  evictedDeviceRowId: true,
  emailSentAt: true,
  createdAt: true,
  expiresAt: true,
} as const;

/** Generiert einen 6-stelligen numerischen Code (000000-999999). */
function generateCode(): string {
  // 0..999999 cryptographically random
  const n = crypto.randomInt(0, 1_000_000);
  return n.toString().padStart(6, "0");
}

function generateEmailToken(): string {
  return crypto.randomBytes(32).toString("hex");
}

/** Hat der User mind. 1 anderes Device das innerhalb des Active-Windows gesehen wurde? */
export async function hasOtherActiveDevices(
  userId: string,
  excludeDeviceId: string,
): Promise<boolean> {
  const db = usePrisma();
  const cutoff = new Date(Date.now() - ACTIVE_DEVICE_WINDOW_MS);
  const count = await db.userDevice.count({
    where: {
      userId,
      deviceId: { not: excludeDeviceId },
      lastSeenAt: { gte: cutoff },
    },
  });
  return count > 0;
}

/**
 * Erstellt einen neuen Approval-Request. Cancelt alle anderen pending Requests
 * für dasselbe newDeviceId (verhindert Spam wenn User mehrfach klickt).
 */
export async function createApprovalRequest(opts: {
  userId: string;
  newDeviceId: string;
  newPlatform: string;
  newModel?: string | null;
  newName?: string | null;
  newOsVersion?: string | null;
}): Promise<DeviceApprovalRecord> {
  const db = usePrisma();

  // Expire alte pending Requests für dieses Device (Cleanup).
  await db.deviceApprovalRequest.updateMany({
    where: {
      userId: opts.userId,
      newDeviceId: opts.newDeviceId,
      status: "pending",
    },
    data: { status: "expired" },
  });

  const code = generateCode();
  const expiresAt = new Date(Date.now() + APPROVAL_TTL_MS);

  return db.deviceApprovalRequest.create({
    data: {
      userId: opts.userId,
      newDeviceId: opts.newDeviceId,
      newPlatform: opts.newPlatform,
      newModel: opts.newModel ?? null,
      newName: opts.newName ?? null,
      newOsVersion: opts.newOsVersion ?? null,
      code,
      expiresAt,
    },
    select: APPROVAL_SELECT,
  });
}

/** Holt einen Approval-Request mit Ownership-Check. */
export async function getApprovalRequest(
  id: string,
  userId: string,
): Promise<DeviceApprovalRecord | null> {
  const db = usePrisma();
  const row = await db.deviceApprovalRequest.findFirst({
    where: { id, userId },
    select: APPROVAL_SELECT,
  });
  if (!row) return null;
  return maybeExpire(row);
}

/** Holt einen Approval-Request per Email-Token (für Email-Link-Approval). */
export async function getApprovalByEmailToken(
  token: string,
): Promise<DeviceApprovalRecord | null> {
  const db = usePrisma();
  const row = await db.deviceApprovalRequest.findUnique({
    where: { emailToken: token },
    select: APPROVAL_SELECT,
  });
  if (!row) return null;
  return maybeExpire(row);
}

/** Listet pending Requests für einen User (für Banner auf existierenden Devices). */
export async function listPendingApprovals(
  userId: string,
): Promise<DeviceApprovalRecord[]> {
  const db = usePrisma();
  const rows = await db.deviceApprovalRequest.findMany({
    where: {
      userId,
      status: "pending",
      expiresAt: { gt: new Date() },
    },
    orderBy: { createdAt: "desc" },
    select: APPROVAL_SELECT,
  });
  return rows;
}

/**
 * Approve: marks request as approved + (atomically) registers the new device,
 * optionally evicting an existing device if the user is at the limit.
 *
 * Returns the approved record. Caller (endpoint) is responsible for the
 * actual UserDevice creation since registerDevice() handles merge-heuristic
 * + slot-check anyway — but we DO evict here if evictDeviceRowId is given
 * so registerDevice doesn't fail with DEVICE_LIMIT_REACHED again.
 */
export async function approveRequest(opts: {
  approvalId: string;
  userId: string;
  approvedByDeviceRowId: string | null; // null = via email-link
  evictDeviceRowId: string | null;
}): Promise<DeviceApprovalRecord | null> {
  const db = usePrisma();

  const existing = await getApprovalRequest(opts.approvalId, opts.userId);
  if (!existing) return null;
  if (existing.status !== "pending") return existing;

  // Eviction transactional with status update.
  return db.$transaction(async (tx) => {
    if (opts.evictDeviceRowId) {
      // Make sure the device-to-evict belongs to the user (defense in depth).
      await tx.userDevice.deleteMany({
        where: { id: opts.evictDeviceRowId, userId: opts.userId },
      });
    }
    return tx.deviceApprovalRequest.update({
      where: { id: opts.approvalId },
      data: {
        status: "approved",
        approvedAt: new Date(),
        approvedByDeviceRowId: opts.approvedByDeviceRowId,
        evictedDeviceRowId: opts.evictDeviceRowId,
      },
      select: APPROVAL_SELECT,
    });
  });
}

export async function rejectRequest(
  approvalId: string,
  userId: string,
): Promise<DeviceApprovalRecord | null> {
  const db = usePrisma();
  const existing = await getApprovalRequest(approvalId, userId);
  if (!existing) return null;
  if (existing.status !== "pending") return existing;

  return db.deviceApprovalRequest.update({
    where: { id: approvalId },
    data: { status: "rejected", rejectedAt: new Date() },
    select: APPROVAL_SELECT,
  });
}

/**
 * Markiert dass eine Email mit Approval-Link verschickt wurde. Generiert
 * (idempotent) einen emailToken den der Link enthält.
 * Rate-Limit: 1 Email pro Approval-Request.
 */
export async function markEmailSent(
  approvalId: string,
  userId: string,
): Promise<DeviceApprovalRecord | null> {
  const db = usePrisma();
  const existing = await getApprovalRequest(approvalId, userId);
  if (!existing) return null;
  if (existing.status !== "pending") return existing;
  if (existing.emailSentAt) return existing; // already sent

  const token = existing.emailToken ?? generateEmailToken();
  return db.deviceApprovalRequest.update({
    where: { id: approvalId },
    data: { emailSentAt: new Date(), emailToken: token },
    select: APPROVAL_SELECT,
  });
}

/** Wenn expiresAt überschritten und status noch pending → setze auf expired. */
async function maybeExpire(
  row: DeviceApprovalRecord,
): Promise<DeviceApprovalRecord> {
  if (row.status !== "pending") return row;
  if (row.expiresAt.getTime() > Date.now()) return row;
  const db = usePrisma();
  const updated = await db.deviceApprovalRequest.update({
    where: { id: row.id },
    data: { status: "expired" },
    select: APPROVAL_SELECT,
  });
  return updated;
}
