/**
 * MFA REST endpoints for SmartAuth.
 *
 * POST /auth/mfa/setup           — generate TOTP secret + QR URI
 * POST /auth/mfa/verify-setup    — verify first TOTP code to activate MFA
 * POST /auth/mfa/verify          — verify TOTP during login challenge flow
 * POST /auth/mfa/disable         — disable MFA (requires valid code)
 * POST /auth/mfa/recovery/regenerate — regenerate recovery codes
 * GET  /auth/mfa/status           — check MFA status for current user
 * GET  /auth/mfa/totp/secret      — retrieve decrypted TOTP secret (for auth app)
 *
 * Admin MFA policies (Phase 2C):
 * GET  /auth/mfa/policies/:productId  — get MFA policy
 * PUT  /auth/mfa/policies/:productId  — create/update MFA policy
 */

import type { FastifyInstance } from 'fastify';
import { BadRequestError, UnauthorizedError, ForbiddenError } from '../../../lib/errors.js';
import * as nodeCrypto from 'node:crypto';
import * as userRepo from '../repository.js';
import * as mfaRepo from './repository.js';
import * as jwt from '../jwt.js';
import { createChallenge, consumeChallenge } from './challenge-store.js';
import {
  MfaVerifySetupSchema,
  MfaVerifySchema,
  MfaDisableSchema,
  MfaPolicySchema,
} from './types.js';
import type { MfaPolicyDoc } from './types.js';

export async function mfaRoutes(app: FastifyInstance) {
  // ── TOTP Setup ──────────────────────────────────────────────

  app.post('/auth/mfa/setup', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    // Check if MFA is already set up
    const existing = await mfaRepo.getByUserId(payload.sub);
    if (existing?.verified) {
      throw new BadRequestError('MFA is already enabled — disable first to re-setup');
    }

    // Generate TOTP secret (base32)
    const secretBuffer = nodeCrypto.randomBytes(20);
    const secret = base32Encode(secretBuffer);

    // Encrypt the secret
    const { encrypted, iv, authTag } = mfaRepo.encryptSecret(secret);

    // Generate recovery codes
    const rawCodes = mfaRepo.generateRecoveryCodes(8);
    const hashedCodes = rawCodes.map(c => mfaRepo.hashRecoveryCode(c));

    const now = new Date().toISOString();
    const doc = {
      id: `mfa_${payload.sub}`,
      userId: payload.sub,
      productId: 'smartauth' as const,
      method: 'totp' as const,
      encryptedSecret: encrypted,
      iv,
      authTag,
      recoveryCodes: hashedCodes,
      verified: false,
      createdAt: now,
      updatedAt: now,
    };

    // Upsert — delete old unverified setup if exists
    if (existing) {
      await mfaRepo.remove(payload.sub);
    }
    await mfaRepo.create(doc);

    const user = await userRepo.getById(payload.sub);
    const issuer = 'ByteLyst';
    const otpauthUri = `otpauth://totp/${issuer}:${user?.email ?? payload.sub}?secret=${secret}&issuer=${issuer}&digits=6&period=30`;

    return {
      secret,
      otpauthUri,
      recoveryCodes: rawCodes,
    };
  });

  // ── Verify Setup (activate MFA) ────────────────────────────

  app.post('/auth/mfa/verify-setup', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const parsed = MfaVerifySetupSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const mfaDoc = await mfaRepo.getByUserId(payload.sub);
    if (!mfaDoc) throw new BadRequestError('MFA setup not found — call /auth/mfa/setup first');
    if (mfaDoc.verified) throw new BadRequestError('MFA is already verified');

    // Decrypt the secret and verify the TOTP code
    const secret = mfaRepo.decryptSecret(mfaDoc.encryptedSecret, mfaDoc.iv, mfaDoc.authTag);
    const valid = verifyTotpCode(secret, parsed.data.code);
    if (!valid) throw new BadRequestError('Invalid TOTP code');

    // Mark as verified
    const now = new Date().toISOString();
    await mfaRepo.update(payload.sub, { verified: true, enabledAt: now });

    // Update user doc
    try {
      await userRepo.update(payload.sub, { mfaEnabled: true, mfaMethods: ['totp'] });
    } catch {
      // best-effort
    }

    req.log.info({ userId: payload.sub }, '[auth] MFA enabled via TOTP');
    return { message: 'MFA enabled successfully' };
  });

  // ── Verify MFA (login challenge flow) ─────────────────────

  app.post('/auth/mfa/verify', async req => {
    const parsed = MfaVerifySchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const challenge = consumeChallenge(parsed.data.challengeToken);
    if (!challenge) throw new UnauthorizedError('Invalid or expired MFA challenge');

    const mfaDoc = await mfaRepo.getByUserId(challenge.userId);
    if (!mfaDoc || !mfaDoc.verified) {
      throw new UnauthorizedError('MFA not configured');
    }

    const code = parsed.data.code;
    let valid = false;

    if (code.length === 6) {
      // TOTP code
      const secret = mfaRepo.decryptSecret(mfaDoc.encryptedSecret, mfaDoc.iv, mfaDoc.authTag);
      valid = verifyTotpCode(secret, code);
    } else {
      // Recovery code
      const hashed = mfaRepo.hashRecoveryCode(code);
      const idx = mfaDoc.recoveryCodes.indexOf(hashed);
      if (idx >= 0) {
        valid = true;
        // Consume the recovery code
        const updatedCodes = [...mfaDoc.recoveryCodes];
        updatedCodes.splice(idx, 1);
        await mfaRepo.update(challenge.userId, { recoveryCodes: updatedCodes });
      }
    }

    if (!valid) throw new UnauthorizedError('Invalid MFA code');

    // Issue tokens
    const accessToken = await jwt.createAccessToken({
      sub: challenge.userId,
      email: challenge.email,
      role: challenge.role,
      productId: challenge.productId,
      plan: challenge.plan as 'free' | 'pro' | 'enterprise',
    });
    const refreshToken = await jwt.createRefreshToken({
      sub: challenge.userId,
      productId: challenge.productId,
    });

    return { accessToken, refreshToken };
  });

  // ── Disable MFA ───────────────────────────────────────────

  app.post('/auth/mfa/disable', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const parsed = MfaDisableSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const mfaDoc = await mfaRepo.getByUserId(payload.sub);
    if (!mfaDoc || !mfaDoc.verified) {
      throw new BadRequestError('MFA is not enabled');
    }

    // Verify code before disabling
    const secret = mfaRepo.decryptSecret(mfaDoc.encryptedSecret, mfaDoc.iv, mfaDoc.authTag);
    const valid = verifyTotpCode(secret, parsed.data.code);
    if (!valid) throw new BadRequestError('Invalid TOTP code');

    await mfaRepo.remove(payload.sub);

    // Update user doc
    try {
      await userRepo.update(payload.sub, { mfaEnabled: false, mfaMethods: [] });
    } catch {
      // best-effort
    }

    req.log.info({ userId: payload.sub }, '[auth] MFA disabled');
    return { message: 'MFA disabled' };
  });

  // ── Regenerate Recovery Codes ─────────────────────────────

  app.post('/auth/mfa/recovery/regenerate', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const mfaDoc = await mfaRepo.getByUserId(payload.sub);
    if (!mfaDoc || !mfaDoc.verified) {
      throw new BadRequestError('MFA is not enabled');
    }

    const rawCodes = mfaRepo.generateRecoveryCodes(8);
    const hashedCodes = rawCodes.map(c => mfaRepo.hashRecoveryCode(c));
    await mfaRepo.update(payload.sub, { recoveryCodes: hashedCodes });

    req.log.info({ userId: payload.sub }, '[auth] Recovery codes regenerated');
    return { recoveryCodes: rawCodes };
  });

  // ── MFA Status ────────────────────────────────────────────

  app.get('/auth/mfa/status', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const mfaDoc = await mfaRepo.getByUserId(payload.sub);
    return {
      enabled: mfaDoc?.verified ?? false,
      method: mfaDoc?.verified ? mfaDoc.method : null,
      enabledAt: mfaDoc?.enabledAt ?? null,
      recoveryCodesRemaining: mfaDoc?.verified ? mfaDoc.recoveryCodes.length : 0,
    };
  });

  // ── TOTP Secret Retrieval (for auth app) ─────────────────

  app.get('/auth/mfa/totp/secret', async req => {
    const payload = req.jwtPayload;
    if (!payload?.sub) throw new UnauthorizedError('Authentication required');

    const mfaDoc = await mfaRepo.getByUserId(payload.sub);
    if (!mfaDoc || !mfaDoc.verified) {
      throw new BadRequestError('MFA is not enabled');
    }

    const secret = mfaRepo.decryptSecret(mfaDoc.encryptedSecret, mfaDoc.iv, mfaDoc.authTag);
    const user = await userRepo.getById(payload.sub);
    const issuer = 'ByteLyst';
    const accountName = user?.email ?? payload.sub;

    return {
      secret,
      issuer,
      accountName,
      digits: 6,
      period: 30,
      algorithm: 'SHA1',
    };
  });

  // ── Admin MFA Policies (Phase 2C) ────────────────────────

  app.get('/auth/mfa/policies/:productId', async req => {
    const role = req.jwtPayload?.role;
    if (!role || !['super_admin', 'admin'].includes(role)) {
      throw new ForbiddenError('Admin access required');
    }
    const { productId } = req.params as { productId: string };
    const policy = await mfaRepo.getPolicy(productId);
    return { policy: policy ?? null };
  });

  app.put('/auth/mfa/policies/:productId', async req => {
    const role = req.jwtPayload?.role;
    if (!role || !['super_admin', 'admin'].includes(role)) {
      throw new ForbiddenError('Admin access required');
    }
    const { productId } = req.params as { productId: string };

    const parsed = MfaPolicySchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const now = new Date().toISOString();
    const doc: MfaPolicyDoc = {
      id: `mfapol_${productId}`,
      productId,
      required: parsed.data.required,
      methods: parsed.data.methods,
      gracePeriodDays: parsed.data.gracePeriodDays,
      createdAt: now,
      updatedAt: now,
    };

    const result = await mfaRepo.upsertPolicy(doc);
    req.log.info({ productId, required: parsed.data.required }, '[auth] MFA policy updated');
    return { policy: result };
  });
}

// ── TOTP helpers (no external dep — RFC 6238 compatible) ────

function base32Encode(buffer: Buffer): string {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  let bits = '';
  for (const byte of buffer) {
    bits += byte.toString(2).padStart(8, '0');
  }
  let result = '';
  for (let i = 0; i < bits.length; i += 5) {
    const chunk = bits.slice(i, i + 5).padEnd(5, '0');
    result += alphabet[parseInt(chunk, 2)];
  }
  return result;
}

function base32Decode(encoded: string): Buffer {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  let bits = '';
  for (const char of encoded.toUpperCase()) {
    const val = alphabet.indexOf(char);
    if (val === -1) continue;
    bits += val.toString(2).padStart(5, '0');
  }
  const bytes: number[] = [];
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.slice(i, i + 8), 2));
  }
  return Buffer.from(bytes);
}

function generateHotp(secret: Buffer, counter: bigint): string {
  const { createHmac } = nodeCrypto;
  const counterBuf = Buffer.alloc(8);
  counterBuf.writeBigUInt64BE(counter);

  const hmac = createHmac('sha1', secret).update(counterBuf).digest();
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code =
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff);

  return (code % 1_000_000).toString().padStart(6, '0');
}

function verifyTotpCode(base32Secret: string, code: string, window = 1): boolean {
  const secret = base32Decode(base32Secret);
  const now = Math.floor(Date.now() / 1000);
  const period = 30;

  for (let i = -window; i <= window; i++) {
    const counter = BigInt(Math.floor(now / period) + i);
    if (generateHotp(secret, counter) === code) {
      return true;
    }
  }
  return false;
}

// Export for testing
export { createChallenge, verifyTotpCode as _verifyTotpCode, base32Encode as _base32Encode };
