import { usePrisma } from "../utils/prisma";

/**
 * Device-Binding pro User. Free=1, Pro=1, Legend=3 (siehe plan-features.maxDevices).
 * deviceId kommt vom Frontend via Capacitor Device.getId() (persistent UUID).
 */

export interface DeviceRecord {
  id: string;
  userId: string;
  deviceId: string;
  platform: string;
  model: string | null;
  name: string | null;
  osVersion: string | null;
  lastSeenAt: Date;
  createdAt: Date;
  // Device-Account-Lock
  boundToPlan: string | null;
  releaseRequestedAt: Date | null;
  lockNotifiedAt: Date | null;
}

/** Pläne die einen Device-Account-Lock aktivieren. Free-User binden nie. */
const LOCKING_PLANS = new Set(["pro", "legend", "standard", "premium"]);

/** Ist ein Plan ein "locking" Plan (Pro/Legend inkl. Legacy-Namen)? */
export function isLockingPlan(plan: string | null | undefined): boolean {
  if (!plan) return false;
  return LOCKING_PLANS.has(plan.toLowerCase());
}

/**
 * Prüft ob ein gegebenes deviceId bereits an einen anderen User gebunden ist
 * (Lock aktiv). Gibt die bound Row zurück wenn ja, null wenn frei.
 *
 * "Gebunden" = boundToPlan ist gesetzt (isLockingPlan) UND kein Release
 * abgelaufen (releaseRequestedAt + 24h <= NOW() = released).
 */
export async function findActiveDeviceLock(
  deviceId: string,
  requestingUserId: string,
): Promise<DeviceRecord | null> {
  const db = usePrisma();

  const row = await db.userDevice.findFirst({
    where: {
      deviceId,
      // Gebunden an einen anderen User
      NOT: { userId: requestingUserId },
      // Binding existiert (Pro/Legend-Account)
      boundToPlan: { not: null },
    },
    select: {
      ...DEVICE_SELECT_WITH_LOCK,
    },
  });

  if (!row) return null;

  // Kein Lock wenn boundToPlan kein Locking-Plan (Sicherheitsnetz, eigentlich
  // schon durch oben gefiltert aber explizit prüfen)
  if (!isLockingPlan(row.boundToPlan)) return null;

  // Wenn Release-Request existiert und 24h abgelaufen → Lock ist released
  if (row.releaseRequestedAt) {
    const releaseAt = new Date(
      row.releaseRequestedAt.getTime() + 24 * 60 * 60 * 1000,
    );
    if (releaseAt <= new Date()) return null;
  }

  // Lock ist aktiv
  return row;
}

/**
 * Bindet ein Device an den User (setzt boundToPlan).
 * Wird nach erfolgreichem Login aufgerufen wenn user.plan ein Locking-Plan ist.
 * Idempotent: wenn bereits gebunden → kein Update.
 */
export async function bindDeviceToUser(
  userId: string,
  deviceId: string,
  plan: string,
): Promise<void> {
  if (!isLockingPlan(plan)) return; // Free-User binden nicht

  const db = usePrisma();
  await db.userDevice.updateMany({
    where: {
      userId,
      deviceId,
      boundToPlan: null, // Nur setzen wenn noch nicht gebunden
    },
    data: {
      boundToPlan: plan,
    },
  });
}

/**
 * Request-Release: Original-User setzt release_requested_at = NOW().
 * Nach 24h ist der Lock automatisch gelöst (Lazy-Check in findActiveDeviceLock).
 * Gibt false zurück wenn Device nicht gefunden oder nicht dem User gehört.
 */
export async function requestDeviceRelease(
  userId: string,
  deviceId: string, // row-id (UUID), nicht deviceId-String
): Promise<boolean> {
  const db = usePrisma();
  const result = await db.userDevice.updateMany({
    where: {
      id: deviceId,
      userId, // Ownership-Check
      boundToPlan: { not: null }, // Muss gebunden sein um freizugeben
    },
    data: {
      releaseRequestedAt: new Date(),
    },
  });
  return result.count > 0;
}

/**
 * Cancel-Release: User zieht den Release-Request zurück.
 * Setzt release_requested_at zurück auf NULL.
 */
export async function cancelDeviceRelease(
  userId: string,
  deviceId: string, // row-id
): Promise<boolean> {
  const db = usePrisma();
  const result = await db.userDevice.updateMany({
    where: {
      id: deviceId,
      userId,
      releaseRequestedAt: { not: null }, // Muss offener Request sein
    },
    data: {
      releaseRequestedAt: null,
    },
  });
  return result.count > 0;
}

/**
 * Setzt lockNotifiedAt für Rate-Limiting der E-Mail-Notifications.
 */
export async function markDeviceLockNotified(rowId: string): Promise<void> {
  const db = usePrisma();
  await db.userDevice
    .update({
      where: { id: rowId },
      data: { lockNotifiedAt: new Date() },
    })
    .catch(() => {});
}

/**
 * Auto-Release: Alle Devices die 30 Tage nicht gesehen wurden UND noch
 * boundToPlan gesetzt haben → boundToPlan zurücksetzen.
 * Wird vom Cron-Plugin (device-lock-cron.ts) aufgerufen.
 *
 * Schützt vor verlorenen/verkauften/defekten Geräten ohne Customer-Support.
 */
export async function autoReleaseInactiveDevices(): Promise<number> {
  const db = usePrisma();
  const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

  const result = await db.userDevice.updateMany({
    where: {
      boundToPlan: { not: null },
      lastSeenAt: { lt: cutoff },
      // Nur wenn kein Release-Request bereits pending (würde in 24h ohnehin ablaufen)
    },
    data: {
      boundToPlan: null,
      releaseRequestedAt: null,
    },
  });

  return result.count;
}

const DEVICE_SELECT = {
  id: true,
  userId: true,
  deviceId: true,
  platform: true,
  model: true,
  name: true,
  osVersion: true,
  lastSeenAt: true,
  createdAt: true,
  boundToPlan: true,
  releaseRequestedAt: true,
  lockNotifiedAt: true,
} as const;

// Alias — identisch mit DEVICE_SELECT, explizit für Lock-Queries
const DEVICE_SELECT_WITH_LOCK = DEVICE_SELECT;

/** Liste aller Devices eines Users, aktuellstes zuerst.
 * Deterministic sort: lastSeenAt DESC, createdAt DESC, id ASC — stellt sicher
 * dass iPad + iPhone die GLEICHE Reihenfolge sehen wenn lastSeenAt identisch ist. */
export async function listUserDevices(userId: string): Promise<DeviceRecord[]> {
  const db = usePrisma();
  return db.userDevice.findMany({
    where: { userId },
    orderBy: [{ lastSeenAt: "desc" }, { createdAt: "desc" }, { id: "asc" }],
    select: DEVICE_SELECT,
  });
}

/**
 * Löscht Phantom-Devices: >14 Tage nicht gesehen + nicht an Plan gebunden.
 * Wird von GET /api/devices + register-Limit-Check aufgerufen damit alte
 * IDFV-Reset-Artifacts nicht das Device-Limit blockieren oder in der UI
 * herumgeistern.
 *
 * Returnt Anzahl gelöschter Rows (für Logging).
 */
export async function cleanupStaleDevices(userId: string): Promise<number> {
  const db = usePrisma();
  const cutoff = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000);
  const res = await db.userDevice.deleteMany({
    where: {
      userId,
      lastSeenAt: { lt: cutoff },
      boundToPlan: null,
    },
  });
  return res.count;
}

/** Gibt das Device zurück wenn registriert; sonst null. */
export async function findUserDevice(
  userId: string,
  deviceId: string,
): Promise<DeviceRecord | null> {
  const db = usePrisma();
  return db.userDevice.findUnique({
    where: { userId_deviceId: { userId, deviceId } },
    select: DEVICE_SELECT,
  });
}

/**
 * Sucht nach einem "Merge-Kandidaten": existierendes Device des Users mit
 * identischem name + model das zuletzt innerhalb der letzten 7 Tage gesehen
 * wurde. Tritt auf wenn iOS IDFV sich nach Recovery-Restore ändert → neuer
 * deviceId, aber gleicher name ("iPhone von Chahine") + gleiches model
 * ("iPhone18,4").
 *
 * 2026-06-01: Cutoff 30d → 7d (verhindert False-Merges nach längerer Inaktivität).
 */
async function findMergeCandidate(
  userId: string,
  name: string | null | undefined,
  model: string | null | undefined,
): Promise<DeviceRecord | null> {
  if (!name || !model) return null;

  const db = usePrisma();
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

  const candidate = await db.userDevice.findFirst({
    where: {
      userId,
      name,
      model,
      lastSeenAt: { gte: cutoff },
    },
    orderBy: { lastSeenAt: "desc" },
    select: DEVICE_SELECT,
  });

  return candidate;
}

/**
 * Idempotente Registrierung. Wenn Device bereits existiert: Touch lastSeenAt.
 * Wenn nicht existiert UND gleicher name+model innerhalb 30d: Merge (IDFV-Reset-Heuristik).
 * Wenn nicht existiert UND Limit erreicht: throw mit Liste der existierenden Devices.
 */
export async function registerDevice(opts: {
  userId: string;
  deviceId: string;
  platform: string;
  model?: string | null;
  name?: string | null;
  osVersion?: string | null;
  maxDevices: number;
}): Promise<{
  device: DeviceRecord;
  created: boolean;
  merged?: boolean;
}> {
  const db = usePrisma();

  // Idempotent: existiert das Device schon?
  const existing = await findUserDevice(opts.userId, opts.deviceId);
  if (existing) {
    // model/name/osVersion beim Re-Register aktualisieren — User-Agent oder OS-Version
    // kann sich geändert haben (App-Update, OS-Upgrade, iPad-Detection-Fix).
    const updated = await db.userDevice.update({
      where: { id: existing.id },
      data: {
        lastSeenAt: new Date(),
        ...(opts.model !== undefined && { model: opts.model }),
        ...(opts.name !== undefined && { name: opts.name }),
        ...(opts.osVersion !== undefined && { osVersion: opts.osVersion }),
      },
      select: DEVICE_SELECT,
    });
    return { device: updated, created: false };
  }

  // Merge-Heuristik: IDFV-Reset nach iOS Recovery-Restore.
  // Wenn name+model matchen und Device zuletzt < 30 Tage gesehen → merge statt insert.
  const mergeTarget = await findMergeCandidate(
    opts.userId,
    opts.name,
    opts.model,
  );
  if (mergeTarget) {
    const merged = await db.userDevice.update({
      where: { id: mergeTarget.id },
      data: {
        deviceId: opts.deviceId, // neue IDFV übernehmen
        lastSeenAt: new Date(),
        ...(opts.osVersion !== undefined && { osVersion: opts.osVersion }),
      },
      select: DEVICE_SELECT,
    });
    return { device: merged, created: false, merged: true };
  }

  // Neues Device — Limit prüfen.
  // 2026-06-01 Auto-Cleanup: alte Phantom-Devices (z.B. nach iOS-Reset mit
  // geändertem Namen, die durch Merge-Heuristik durchgerutscht sind) blockieren
  // sonst das Limit.
  await cleanupStaleDevices(opts.userId);

  const count = await db.userDevice.count({ where: { userId: opts.userId } });
  if (count >= opts.maxDevices) {
    throw Object.assign(new Error("device_limit_reached"), {
      code: "DEVICE_LIMIT_REACHED",
      currentCount: count,
      max: opts.maxDevices,
    });
  }

  const created = await db.userDevice.create({
    data: {
      userId: opts.userId,
      deviceId: opts.deviceId,
      platform: opts.platform,
      model: opts.model ?? null,
      name: opts.name ?? null,
      osVersion: opts.osVersion ?? null,
    },
    select: DEVICE_SELECT,
  });
  return { device: created, created: true };
}

/** Touch lastSeenAt — wird in der Auth-Middleware bei jedem Request aufgerufen.
 * Optional: backfillt name/model/osVersion wenn Headers da sind (für Devices
 * die unter altem Build registriert wurden ohne diese Info). */
export async function touchDevice(
  userId: string,
  deviceId: string,
  info?: {
    name?: string | null;
    model?: string | null;
    osVersion?: string | null;
  },
): Promise<void> {
  const db = usePrisma();
  const data: {
    lastSeenAt: Date;
    name?: string;
    model?: string;
    osVersion?: string;
  } = {
    lastSeenAt: new Date(),
  };
  if (info?.name) data.name = info.name;
  if (info?.model) data.model = info.model;
  if (info?.osVersion) data.osVersion = info.osVersion;
  await db.userDevice
    .updateMany({
      where: { userId, deviceId },
      data,
    })
    .catch(() => {
      /* race-safe: wenn Device gerade gelöscht wurde */
    });
}

/** User entfernt ein eigenes Device — gibt Slot frei. */
export async function deleteUserDevice(
  userId: string,
  id: string,
): Promise<void> {
  const db = usePrisma();
  await db.userDevice.deleteMany({ where: { id, userId } });
}

// ─────────────────────────────────────────────────────────────────────────────
// RebreakMagic DNS-Device-Binding
// ─────────────────────────────────────────────────────────────────────────────

// Magic-Binding-Limit ist plan-gated (plan-features.maxProtectedDevices:
// Pro 1 / Legend 2) — geprüft in api/magic/register.post.ts.

export interface MagicDeviceRecord {
  deviceId: string;
  hostname: string | null;
  model: string | null;
  osVersion: string | null;
  mdmId: string | null;
  magicEnrolledAt: Date;
  releaseRequestedAt: Date | null;
  magicRevokedAt: Date | null;
  magicCooldownUntil: Date | null;
  lastSeenAt: Date | null;
}

/**
 * Listet alle aktiven Magic-Bindings eines Users.
 * Aktiv = magicEnrolledAt != null AND magicRevokedAt == null.
 */
export async function listMagicDevices(
  userId: string,
): Promise<MagicDeviceRecord[]> {
  const db = usePrisma();
  const devices = await db.userDevice.findMany({
    where: {
      userId,
      magicEnrolledAt: { not: null },
      magicRevokedAt: null,
    },
    orderBy: { magicEnrolledAt: "desc" },
    select: {
      deviceId: true,
      magicHostname: true,
      model: true,
      osVersion: true,
      mdmId: true,
      magicEnrolledAt: true,
      releaseRequestedAt: true,
      magicRevokedAt: true,
      magicCooldownUntil: true,
      lastSeenAt: true,
    },
  });

  return devices.map((d) => ({
    deviceId: d.deviceId,
    hostname: d.magicHostname,
    model: d.model,
    osVersion: d.osVersion,
    mdmId: d.mdmId,
    magicEnrolledAt: d.magicEnrolledAt!,
    releaseRequestedAt: d.releaseRequestedAt,
    magicRevokedAt: d.magicRevokedAt,
    magicCooldownUntil: d.magicCooldownUntil,
    lastSeenAt: d.lastSeenAt,
  }));
}

/**
 * Zählt aktive Magic-Bindings für Limit-Check.
 */
export async function countActiveMagicBindings(
  userId: string,
): Promise<number> {
  const db = usePrisma();
  return db.userDevice.count({
    where: {
      userId,
      magicEnrolledAt: { not: null },
      magicRevokedAt: null,
    },
  });
}

/**
 * Findet Device anhand DNS-Token. Nur aktive Tokens (nicht revoked).
 */
export async function findMagicDeviceByToken(
  token: string,
): Promise<
  | (DeviceRecord & {
      magicDnsToken: string;
      magicEnrolledAt: Date | null;
      magicRevokedAt: Date | null;
      magicCooldownUntil: Date | null;
      magicRemovalPassword: string | null;
    })
  | null
> {
  const db = usePrisma();
  const device = await db.userDevice.findUnique({
    where: {
      magicDnsToken: token,
    },
    select: {
      ...DEVICE_SELECT,
      magicDnsToken: true,
      magicEnrolledAt: true,
      magicRevokedAt: true,
      magicCooldownUntil: true,
      magicHostname: true,
      magicRemovalPassword: true,
    },
  });

  if (!device) return null;
  if (device.magicRevokedAt) return null; // Token invalidiert

  return {
    ...device,
    magicDnsToken: device.magicDnsToken!,
  };
}

/**
 * Setzt ein Removal-Passwort nachträglich (Lazy-Backfill für Magic-Devices,
 * die vor dem Hard-Lock gebunden wurden). Idempotent über das WHERE-Guard.
 */
export async function ensureMagicRemovalPassword(
  rowId: string,
  password: string,
): Promise<void> {
  const db = usePrisma();
  await db.userDevice.updateMany({
    where: { id: rowId, magicRemovalPassword: null },
    data: { magicRemovalPassword: password },
  });
}

export interface MagicRemovalCredential {
  hostname: string | null;
  model: string | null;
  removalPassword: string;
}

/**
 * Removal-Credentials aller aktiven Magic-Devices eines Users — NUR für den
 * Reveal bei Kündigung/Account-Löschung (Offboarding). Liefert das server-
 * gehaltene Passwort, damit der User die gesperrten Profile entfernen kann.
 */
export async function listMagicRemovalCredentials(
  userId: string,
): Promise<MagicRemovalCredential[]> {
  const db = usePrisma();
  const devices = await db.userDevice.findMany({
    where: {
      userId,
      magicEnrolledAt: { not: null },
      magicRevokedAt: null,
      magicRemovalPassword: { not: null },
    },
    select: {
      magicHostname: true,
      model: true,
      magicRemovalPassword: true,
    },
  });
  return devices.map((d) => ({
    hostname: d.magicHostname,
    model: d.model,
    removalPassword: d.magicRemovalPassword!,
  }));
}
