import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { getEnv } from '@/shared/config/env-validator';

const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

function base32Encode(buffer: Buffer): string {
  let bits = 0;
  let value = 0;
  let output = '';

  for (const byte of buffer) {
    value = (value << 8) | byte;
    bits += 8;

    while (bits >= 5) {
      output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }

  if (bits > 0) {
    output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
  }

  return output;
}

function base32Decode(value: string): Buffer {
  const normalized = value.replace(/=+$/g, '').replace(/[\s-]/g, '').toUpperCase();
  let bits = 0;
  let accumulator = 0;
  const bytes: number[] = [];

  for (const char of normalized) {
    const index = BASE32_ALPHABET.indexOf(char);
    if (index === -1) {
      throw new Error('Invalid base32 secret');
    }

    accumulator = (accumulator << 5) | index;
    bits += 5;

    if (bits >= 8) {
      bytes.push((accumulator >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }

  return Buffer.from(bytes);
}

function hotp(secret: Buffer, counter: number, digits = 6): string {
  const counterBuffer = Buffer.alloc(8);
  counterBuffer.writeBigUInt64BE(BigInt(counter));
  const digest = crypto.createHmac('sha1', secret).update(counterBuffer).digest();
  const offset = digest[digest.length - 1] & 0x0f;
  const code = ((digest[offset] & 0x7f) << 24)
    | ((digest[offset + 1] & 0xff) << 16)
    | ((digest[offset + 2] & 0xff) << 8)
    | (digest[offset + 3] & 0xff);
  return (code % 10 ** digits).toString().padStart(digits, '0');
}

export class PortalSecurityService {
  private readonly encryptionKey = crypto
    .createHash('sha256')
    .update(getEnv('JWT_SECRET'))
    .digest();

  hashPassword(password: string): string {
    this.ensureStrongPassword(password);
    return bcrypt.hashSync(password, 12);
  }

  verifyPassword(password: string, passwordHash: string): boolean {
    return bcrypt.compareSync(password, passwordHash);
  }

  ensureStrongPassword(password: string): void {
    const trimmed = password.trim();
    if (trimmed.length < 14) {
      throw new Error('Password must be at least 14 characters long');
    }

    const checks = [
      /[a-z]/.test(trimmed),
      /[A-Z]/.test(trimmed),
      /\d/.test(trimmed),
      /[^A-Za-z0-9]/.test(trimmed),
    ];

    if (checks.some((passed) => !passed)) {
      throw new Error(
        'Password must include upper-case, lower-case, numeric, and special characters'
      );
    }
  }

  generateResetToken(): { plaintext: string; hash: string } {
    const plaintext = crypto.randomBytes(24).toString('base64url');
    return {
      plaintext,
      hash: this.hashResetToken(plaintext),
    };
  }

  hashResetToken(token: string): string {
    return crypto.createHash('sha256').update(token).digest('hex');
  }

  generateTotpSecret(): string {
    return base32Encode(crypto.randomBytes(20));
  }

  generateTotpUri(label: string, secret: string): string {
    const issuer = encodeURIComponent(process.env.DBIS_PORTAL_MFA_ISSUER || 'DBIS Portal');
    const account = encodeURIComponent(label);
    return `otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}&algorithm=SHA1&digits=6&period=30`;
  }

  verifyTotp(secret: string, code: string, window = 1): boolean {
    const normalizedCode = code.replace(/\s+/g, '');
    if (!/^\d{6}$/.test(normalizedCode)) {
      return false;
    }

    const secretBuffer = base32Decode(secret);
    const currentCounter = Math.floor(Date.now() / 1000 / 30);

    for (let offset = -window; offset <= window; offset += 1) {
      const candidate = hotp(secretBuffer, currentCounter + offset);
      if (
        crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(normalizedCode))
      ) {
        return true;
      }
    }

    return false;
  }

  encryptMfaSecret(secret: string): { ciphertext: string; iv: string; tag: string } {
    const iv = crypto.randomBytes(12);
    const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
    const ciphertext = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
    const tag = cipher.getAuthTag();

    return {
      ciphertext: ciphertext.toString('base64'),
      iv: iv.toString('base64'),
      tag: tag.toString('base64'),
    };
  }

  decryptMfaSecret(ciphertext: string, iv: string, tag: string): string {
    const decipher = crypto.createDecipheriv(
      'aes-256-gcm',
      this.encryptionKey,
      Buffer.from(iv, 'base64')
    );
    decipher.setAuthTag(Buffer.from(tag, 'base64'));
    const plaintext = Buffer.concat([
      decipher.update(Buffer.from(ciphertext, 'base64')),
      decipher.final(),
    ]);
    return plaintext.toString('utf8');
  }
}

export const portalSecurityService = new PortalSecurityService();
