/**
 * TOTP AuthProvider — the original DashCaddy login method.
 *
 * Two methods are exposed:
 *
 *   totp-code  — challenge-response. User enters a 6-digit code from their
 *                authenticator app. /initiate is a no-op (the UI already has
 *                the code input), /verify checks the code + creates session.
 *
 *   totp-setup — one-time enrollment. /initiate generates the secret + QR,
 *                /verify confirms the first valid code from that secret and
 *                flips the provider into enabled state.
 *
 * TOTP-specific maintenance endpoints (disable, recovery-info, change
 * sessionDuration) live alongside the methods but are routed through the
 * provider's other methods rather than the /login namespace — they're admin
 * actions, not user login flows. The legacy /api/v1/totp/* routes in
 * routes/auth/totp.js remain as thin pass-throughs to this provider so old
 * frontends keep working.
 */

const { authenticator } = require('otplib');
const QRCode = require('qrcode');
const AuthProvider = require('./base');
const { ValidationError, AuthenticationError } = require('../../utilities/errors');
const { ok, successMessage } = require('../../utils/responses');

const SETUP_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const SETUP_LIMIT = 10;

class TotpProvider extends AuthProvider {
  constructor(deps) {
    super(deps);
    this.name = 'totp';
    // Per-IP rate limit on /initiate → setup (secret generation)
    this._setupAttempts = new Map();
  }

  // ── Public state ─────────────────────────────────────────────────────────

  async getConfig() {
    return {
      enabled: this.deps.config.enabled,
      sessionDuration: this.deps.config.sessionDuration,
      isSetUp: this.deps.config.isSetUp,
    };
  }

  async listMethods() {
    // Only surface setup UI if TOTP isn't yet set up — once it's live,
    // totp-code is the only user-facing flow.
    if (!this.deps.config.isSetUp) {
      return [
        {
          id: 'totp-setup',
          label: 'Set up TOTP',
          description: 'Configure a new authenticator app',
        },
      ];
    }
    if (!this.deps.config.enabled) {
      // TOTP is configured but the operator disabled it. Login is closed.
      return [];
    }
    return [
      {
        id: 'totp-code',
        label: 'Enter TOTP code',
        description: '6-digit code from your authenticator app',
      },
    ];
  }

  async isSetUp() { return this.deps.config.isSetUp === true; }
  async isEnabled() { return this.deps.config.enabled === true && this.deps.config.isSetUp === true; }

  async recoveryInfo() {
    if (!this.deps.config.isSetUp) {
      return {
        status: 'not_configured',
        isSetUp: false,
        hint: 'TOTP has not been set up on this server yet. Open settings to configure it.',
      };
    }
    const diag = await this.deps.credentialManager.diagnose('totp.secret');
    if (diag.status === 'ok') {
      return {
        status: 'healthy',
        isSetUp: true,
        hint: 'TOTP is configured and the stored secret is readable. Enter your authenticator code to log in.',
      };
    }
    if (diag.status === 'unreadable') {
      return {
        status: 'unreadable',
        isSetUp: true,
        hint: 'TOTP secret on disk cannot be decrypted with the current encryption key. The key was rotated after setup — you must re-set up TOTP.',
      };
    }
    return {
      status: 'corrupt',
      isSetUp: true,
      hint: 'TOTP entry exists but is malformed. Re-set up TOTP.',
    };
  }

  async setConfig(updates) {
    if (updates.sessionDuration !== undefined) {
      if (!Object.prototype.hasOwnProperty.call(this.deps.session.durations, updates.sessionDuration)) {
        throw new ValidationError(
          `Invalid session duration. Valid options: ${Object.keys(this.deps.session.durations).join(', ')}`,
          'sessionDuration'
        );
      }
      this.deps.config.sessionDuration = updates.sessionDuration;
      if (updates.sessionDuration === 'never') this.deps.config.enabled = false;
    }
    await this.deps.saveProviderConfig();
    return this.getConfig();
  }

  // ── initiate / verify ────────────────────────────────────────────────────

  async initiate(methodId, req, res) {
    if (methodId === 'totp-setup') {
      return this._initiateSetup(req, res);
    }
    if (methodId === 'totp-code') {
      // No challenge to send — the UI already has the code input box.
      return ok(res, { challenge: 'code' });
    }
    throw new ValidationError(`Unknown TOTP method: ${methodId}`, 'methodId');
  }

  async verify(methodId, req, res) {
    if (methodId === 'totp-setup') {
      return this._verifySetup(req, res);
    }
    if (methodId === 'totp-code') {
      return this._verifyCode(req, res);
    }
    throw new ValidationError(`Unknown TOTP method: ${methodId}`, 'methodId');
  }

  async disable(req, res) {
    // Always require a valid TOTP code when TOTP is active.
    if (this.deps.config.enabled && this.deps.config.isSetUp) {
      const { code } = req.body || {};
      if (!code || !/^\d{6}$/.test(code)) {
        throw new ValidationError('A valid TOTP code is required to disable TOTP', 'code');
      }
      const secret = await this.deps.credentialManager.retrieve('totp.secret');
      if (secret) {
        authenticator.options = { window: 1 };
        if (!authenticator.verify({ token: code, secret })) {
          throw new AuthenticationError('[DC-111] Invalid code');
        }
      }
    }
    await this.deps.credentialManager.delete('totp.secret');
    await this.deps.credentialManager.delete('totp.pending_secret');

    this.deps.config.enabled = false;
    this.deps.config.isSetUp = false;
    this.deps.config.sessionDuration = 'never';
    delete this.deps.config.secret;
    await this.deps.saveProviderConfig();

    this.deps.session.clear(req);
    this.deps.session.clearCookie(res);
    successMessage(res, 'TOTP disabled');
  }

  // ── Setup path (totp-setup method) ──────────────────────────────────────

  async _initiateSetup(req, res) {
    const ip = this._clientIP(req);
    const now = Date.now();
    const recent = (this._setupAttempts.get(ip) || []).filter(t => now - t < SETUP_WINDOW_MS);
    if (recent.length >= SETUP_LIMIT) {
      return res.status(429).json({
        success: false,
        error: 'Too many setup attempts. Try again in an hour.',
        code: 'DC-429',
      });
    }
    recent.push(now);
    this._setupAttempts.set(ip, recent);

    let secret;
    if (req.body && req.body.secret) {
      secret = req.body.secret.replace(/\s/g, '').toUpperCase();
      // Normalize common Base32 confusions: 0→O, 1→L, 8→B
      secret = secret.replace(/0/g, 'O').replace(/1/g, 'L').replace(/8/g, 'B');
      if (!/^[A-Z2-7]{16,}$/.test(secret)) {
        throw new ValidationError(
          'Invalid secret key format. Must be a Base32 string (letters A-Z and digits 2-7).',
          'secret'
        );
      }
    } else {
      secret = authenticator.generateSecret();
    }
    await this.deps.credentialManager.store('totp.pending_secret', secret);

    const otpauth = authenticator.keyuri('user', 'DashCaddy', secret);
    const qrDataUrl = await QRCode.toDataURL(otpauth, {
      width: 256, margin: 2,
      color: { dark: '#ffffff', light: '#00000000' },
    });

    ok(res, {
      qrCode: qrDataUrl,
      manualKey: secret,
      issuer: 'DashCaddy',
      imported: !!req.body?.secret,
    });
  }

  async _verifySetup(req, res) {
    const { code } = req.body || {};
    if (!code || !/^\d{6}$/.test(code)) {
      throw new ValidationError('Invalid code format', 'code');
    }
    const pendingSecret = await this.deps.credentialManager.retrieve('totp.pending_secret');
    if (!pendingSecret) {
      throw new ValidationError('No pending TOTP setup. Call /api/auth/login/totp/initiate first.');
    }
    authenticator.options = { window: 1 };
    if (!authenticator.verify({ token: code, secret: pendingSecret })) {
      throw new AuthenticationError('[DC-111] Invalid code. Please try again.');
    }
    // Promote pending secret to active
    await this.deps.credentialManager.store('totp.secret', pendingSecret);
    await this.deps.credentialManager.delete('totp.pending_secret');

    this.deps.config.isSetUp = true;
    this.deps.config.enabled = true;
    if (this.deps.config.sessionDuration === 'never') {
      this.deps.config.sessionDuration = '24h';
    }
    await this.deps.saveProviderConfig();

    this.deps.session.create(req, this.deps.config.sessionDuration);
    this.deps.session.setCookie(res, this.deps.config.sessionDuration);

    ok(res, {
      message: 'TOTP enabled successfully',
      sessionDuration: this.deps.config.sessionDuration,
    });
  }

  // ── Login path (totp-code method) ───────────────────────────────────────

  async _verifyCode(req, res) {
    const { code } = req.body || {};
    if (!code || !/^\d{6}$/.test(code)) {
      throw new ValidationError('Invalid code format', 'code');
    }
    if (!this.deps.config.enabled || !this.deps.config.isSetUp) {
      throw new ValidationError('TOTP is not enabled');
    }
    const secret = await this.deps.credentialManager.retrieve('totp.secret');
    if (!secret) throw new Error('TOTP secret not found');

    authenticator.options = { window: 1 };
    if (!authenticator.verify({ token: code, secret })) {
      throw new AuthenticationError('[DC-111] Invalid code');
    }
    this.deps.log.info('auth', 'TOTP verified, creating session', {
      ip: this._clientIP(req),
      duration: this.deps.config.sessionDuration,
    });
    this.deps.session.create(req, this.deps.config.sessionDuration);
    this.deps.session.setCookie(res, this.deps.config.sessionDuration);

    // DC-048: bootstrap-on-first-TOTP-verify. If no user store is wired
    // (legacy install), skip silently — operator keeps anonymous access.
    // If a user store IS wired and bootstrap hasn't happened yet, create
    // a "system-admin" record tied to this TOTP login so the operator
    // shows up in /api/v1/auth/admin/users. Email is null because TOTP
    // has no email to attribute.
    if (this.deps.userStore) {
      const isBootstrapped = await this.deps.userStore.isBootstrapComplete();
      if (!isBootstrapped) {
        const result = await this.deps.userStore.login({
          email: 'system@totp.local',
          ip: this._clientIP(req),
          displayName: 'Operator (TOTP)',
        });
        if (result.ok) {
          req.user = {
            id: result.user.id,
            email: null,
            role: result.user.role,
            isAdmin: result.user.role === 'admin',
            isBootstrap: result.isBootstrap,
            viaProvider: 'totp',
          };
          this.deps.log.info('auth', 'system admin bootstrapped via TOTP', {
            userId: result.user.id,
            role: result.user.role,
          });
        }
      } else {
        // Bootstrap already happened — find the system-admin record and
        // attach it to this session for audit-log attribution.
        const sys = await this.deps.userStore.getUserByEmail('system@totp.local');
        if (sys) {
          req.user = {
            id: sys.id,
            email: null,
            role: sys.role,
            isAdmin: sys.role === 'admin',
            isBootstrap: false,
            viaProvider: 'totp',
          };
        }
      }
    }

    const newCsrfToken = this.deps.renewCSRFToken(res, req.secure || req.protocol === 'https');
    this.deps.log.debug('auth', 'Session created', { sessions: this.deps.session.ipSessions.size });

    ok(res, {
      message: 'Authenticated successfully',
      sessionDuration: this.deps.config.sessionDuration,
      csrfToken: newCsrfToken,
    });
  }

  _clientIP(req) {
    const s = this.deps.session;
    if (s && typeof s.getClientIP === 'function') return s.getClientIP(req);
    return req.ip || req.socket?.remoteAddress || 'unknown';
  }
}

module.exports = TotpProvider;