import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { randomBytes } from 'node:crypto';
import type { Request, Response, NextFunction } from 'express';

const SALT_ROUNDS = 10;
const TOKEN_EXPIRY = '24h';
const MAX_ATTEMPTS = 5;
const RATE_WINDOW_MS = 60000;

interface AttemptRecord {
  count: number;
  firstAttempt: number;
}

export class AuthManager {
  private passwordHash: string | null = null;
  private jwtSecret: string;
  private attempts = new Map<string, AttemptRecord>();
  private authDisabled = false;
  private cleanupInterval: ReturnType<typeof setInterval>;

  constructor() {
    this.jwtSecret = randomBytes(32).toString('hex');
    this.cleanupInterval = setInterval(() => this.sweepExpiredAttempts(), 5 * 60 * 1000);
    this.cleanupInterval.unref();
  }

  destroy(): void {
    clearInterval(this.cleanupInterval);
  }

  private sweepExpiredAttempts(): void {
    const now = Date.now();
    for (const [ip, record] of this.attempts) {
      if (now - record.firstAttempt > RATE_WINDOW_MS) {
        this.attempts.delete(ip);
      }
    }
  }

  disableAuth(): void {
    this.authDisabled = true;
  }

  isAuthDisabled(): boolean {
    return this.authDisabled;
  }

  async setPassword(password: string): Promise<void> {
    this.passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
  }

  async verifyPassword(password: string): Promise<boolean> {
    if (!this.passwordHash) return false;
    return bcrypt.compare(password, this.passwordHash);
  }

  generateToken(): string {
    return jwt.sign({ ts: Date.now() }, this.jwtSecret, { expiresIn: TOKEN_EXPIRY });
  }

  verifyToken(token: string): boolean {
    if (this.authDisabled) return true;
    try {
      jwt.verify(token, this.jwtSecret);
      return true;
    } catch {
      return false;
    }
  }

  isRateLimited(ip: string): boolean {
    const record = this.attempts.get(ip);
    if (!record) return false;

    // Reset if window expired
    if (Date.now() - record.firstAttempt > RATE_WINDOW_MS) {
      this.attempts.delete(ip);
      return false;
    }

    return record.count >= MAX_ATTEMPTS;
  }

  recordAttempt(ip: string): void {
    const record = this.attempts.get(ip);
    if (!record || Date.now() - record.firstAttempt > RATE_WINDOW_MS) {
      this.attempts.set(ip, { count: 1, firstAttempt: Date.now() });
    } else {
      record.count++;
    }
  }

  resetAttempts(ip: string): void {
    this.attempts.delete(ip);
  }

  loginHandler() {
    return async (req: Request, res: Response): Promise<void> => {
      const ip = req.ip || req.socket.remoteAddress || 'unknown';

      if (this.isRateLimited(ip)) {
        res.status(429).json({ error: 'Too many attempts. Try again later.' });
        return;
      }

      const { password } = req.body as { password?: string };
      if (!password) {
        res.status(400).json({ error: 'Password required' });
        return;
      }

      this.recordAttempt(ip);

      const valid = await this.verifyPassword(password);
      if (!valid) {
        console.log(`[auth] Failed login from ${ip}`);
        res.status(401).json({ error: 'Invalid password' });
        return;
      }

      this.resetAttempts(ip);
      const token = this.generateToken();
      console.log(`[auth] Successful login from ${ip}`);
      res.json({ token });
    };
  }

  cspMiddleware() {
    return (_req: Request, res: Response, next: NextFunction): void => {
      res.setHeader(
        'Content-Security-Policy',
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data:;"
      );
      res.setHeader('X-Content-Type-Options', 'nosniff');
      res.setHeader('X-Frame-Options', 'DENY');
      res.setHeader('Referrer-Policy', 'no-referrer');
      next();
    };
  }
}

export function generatePassphrase(): string {
  const words = [
    'alpha', 'bravo', 'cedar', 'delta', 'ember', 'frost', 'grape', 'hatch',
    'ivory', 'junco', 'knack', 'lunar', 'maple', 'north', 'orbit', 'pearl',
    'quilt', 'ridge', 'solar', 'thorn', 'ultra', 'vivid', 'watch', 'xenon',
  ];
  const picked: string[] = [];
  const bytes = randomBytes(4);
  for (let i = 0; i < 4; i++) {
    picked.push(words[bytes[i] % words.length]);
  }
  return picked.join('-');
}
