/**
 * UserTwoFactor Model
 *
 * One document per user who has started or completed two-factor enrolment.
 * Holds the TOTP shared secret (sealed with AES-256-GCM), the hashed recovery
 * codes, and the counters that stop brute-force and replay attacks.
 *
 * Deliberately a separate collection rather than fields on `User`: the User
 * schema's `toJSON` transform only strips `passwordHash`, so anything stored
 * there is serialised straight out of `/auth/me`, `/super-admin/users` and every
 * other endpoint that returns a user. A second-factor secret must never be able
 * to leak through a response body by accident.
 */

import mongoose, { Schema, Document, Types } from 'mongoose';

export type TwoFactorStatus = 'pending' | 'enabled' | 'disabled';

/** The factor this user actually enrolled with. */
export type TwoFactorMethod = 'totp' | 'email';

export interface IRecoveryCodeEntry {
  /** bcrypt hash — the plaintext is shown once at generation and never stored. */
  codeHash: string;
  usedAt: Date | null;
}

export interface IUserTwoFactor extends Document {
  userId: Types.ObjectId;
  /** `pending` = enrolment started, first code not yet confirmed. Only `enabled` challenges a login. */
  status: TwoFactorStatus;
  /**
   * Which factor this account is enrolled with. Defaults to `totp`, so every
   * document written before email OTP existed is already correct — that default
   * IS the migration.
   */
  method: TwoFactorMethod;
  /**
   * TOTP secret, sealed by services/auth/secretBox (format `iv:tag:ciphertext`).
   * Absent for an email-OTP enrolment, which has no shared secret.
   */
  secretCiphertext?: string;
  algorithm: 'SHA1' | 'SHA256' | 'SHA512';
  digits: number;
  period: number;
  recoveryCodes: IRecoveryCodeEntry[];
  recoveryCodesGeneratedAt?: Date;
  /**
   * Highest TOTP time step already accepted for this user. A code is only valid
   * if its step is strictly greater, so an observed code cannot be replayed for
   * the remainder of its validity window.
   */
  lastUsedTimeStep: number;
  /** How many times enrolment has been deferred. Drives maxSkips and is
   *  visible to an admin, so 'who keeps dodging this' is answerable. */
  skipCount: number;
  lastSkippedAt?: Date;
  confirmedAt?: Date;
  lastVerifiedAt?: Date;
  /** Consecutive failures. Cleared on success. Drives `lockedUntil`. */
  failedAttempts: number;
  lockedUntil?: Date | null;
  /** Super Admin reset trail — who wiped this enrolment and when. */
  resetByUserId?: Types.ObjectId;
  resetAt?: Date;
  createdAt: Date;
  updatedAt: Date;
}

const RecoveryCodeSchema = new Schema<IRecoveryCodeEntry>({
  codeHash: { type: String, required: true },
  usedAt: { type: Date, default: null },
}, { _id: false });

const UserTwoFactorSchema = new Schema<IUserTwoFactor>({
  userId: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    unique: true,
    index: true,
  },
  status: {
    type: String,
    enum: ['pending', 'enabled', 'disabled'],
    default: 'pending',
    index: true,
  },
  method: {
    type: String,
    enum: ['totp', 'email'],
    default: 'totp',
    index: true,
  },
  secretCiphertext: {
    type: String,
    // Required only for TOTP. An email-OTP enrolment stores no secret at all —
    // there is nothing shared to keep. Existing documents are all TOTP and all
    // carry a secret, so this relaxation invalidates nothing.
    required: function (this: any) { return this.method === 'totp'; },
    // Never returned by a default query — it has to be asked for explicitly
    // with .select('+secretCiphertext'), so a stray .lean() cannot leak it.
    select: false,
  },
  algorithm: {
    type: String,
    enum: ['SHA1', 'SHA256', 'SHA512'],
    default: 'SHA1',
  },
  digits: { type: Number, default: 6, min: 6, max: 8 },
  period: { type: Number, default: 30, min: 15, max: 120 },
  recoveryCodes: {
    type: [RecoveryCodeSchema],
    default: [],
    select: false,
  },
  recoveryCodesGeneratedAt: { type: Date },
  lastUsedTimeStep: { type: Number, default: 0 },
  skipCount: { type: Number, default: 0 },
  lastSkippedAt: { type: Date },
  confirmedAt: { type: Date },
  lastVerifiedAt: { type: Date },
  failedAttempts: { type: Number, default: 0 },
  lockedUntil: { type: Date, default: null },
  resetByUserId: { type: Schema.Types.ObjectId, ref: 'User' },
  resetAt: { type: Date },
}, {
  timestamps: true,
  toJSON: {
    // Belt and braces alongside `select: false` — if one of these ever ends up
    // on a hydrated document, it still must not serialise.
    transform: (_doc, ret) => {
      delete (ret as any).secretCiphertext;
      delete (ret as any).recoveryCodes;
      return ret;
    },
  },
});

UserTwoFactorSchema.index({ userId: 1 }, { unique: true });
UserTwoFactorSchema.index({ status: 1 });

export const UserTwoFactor =
  mongoose.models.UserTwoFactor ||
  mongoose.model<IUserTwoFactor>('UserTwoFactor', UserTwoFactorSchema);
