import crypto from 'node:crypto';
import { readFileSync } from 'node:fs';
import http from 'node:http';
import { isIP } from 'node:net';

import { authenticateWithRateLimit, authenticationRateLimiter } from './auth-rate-limit.js';
import { verifyInboundMailboxCredential } from './db.js';

const authPath = '/internal/dovecot/auth';
const defaultBodyLimit = 8 * 1024;
const defaultRequestTimeoutMs = 5_000;

export function createDovecotAuthServer(options = {}) {
  const sharedSecretDigest = digestSecret(readSharedSecret(options.secretFile));
  const limiter = options.authRateLimiter || authenticationRateLimiter;
  const verifyCredential = options.verifyCredential || verifyInboundMailboxCredential;
  const logger = options.logger || console;
  const requestTimeoutMs = positiveInteger(options.requestTimeoutMs, defaultRequestTimeoutMs);

  const server = http.createServer((req, res) => {
    void handleRequest(req, res, {
      sharedSecretDigest,
      limiter,
      verifyCredential,
      logger,
      bodyLimit: defaultBodyLimit
    });
  });
  server.requestTimeout = requestTimeoutMs;
  server.headersTimeout = requestTimeoutMs;
  server.keepAliveTimeout = 1_000;
  return server;
}

export function startDovecotAuthServer(options = {}) {
  const server = createDovecotAuthServer(options);
  const host = String(options.host || '0.0.0.0');
  const port = Number(options.port ?? 3001);
  server.listen(port, host, () => {
    options.onListening?.(server);
  });
  return server;
}

async function handleRequest(req, res, context) {
  setPrivateHeaders(res);
  const pathname = requestPathname(req);
  if (pathname !== authPath) return sendJson(res, 404, { error: 'Not found.' });
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    return sendJson(res, 405, { error: 'Method not allowed.' });
  }
  if (!validBearerSecret(req.headers.authorization, context.sharedSecretDigest)) {
    return sendJson(res, 401, { error: 'Unauthorized.' });
  }
  if (requestContentType(req) !== 'application/json') {
    return sendJson(res, 415, { error: 'Unsupported media type.' });
  }

  const contentLength = parseContentLength(req.headers['content-length']);
  if (contentLength === null) return sendJson(res, 400, { error: 'Invalid request.' });
  if (contentLength > context.bodyLimit) return sendTooLarge(req, res);

  let body;
  try {
    body = await readJson(req, context.bodyLimit);
  } catch (error) {
    if (error instanceof RequestTooLargeError) return sendTooLarge(req, res);
    return sendJson(res, 400, { error: 'Invalid request.' });
  }

  const request = normalizeAuthRequest(body);
  if (!request) return sendJson(res, 400, { error: 'Invalid request.' });

  try {
    const authenticated = authenticateWithRateLimit({
      limiter: context.limiter,
      ip: request.remoteIp,
      account: request.username,
      authenticate: () => context.verifyCredential(request.username, request.password)
    });
    if (!authenticated) return sendJson(res, 200, { authenticated: false });
    const user = canonicalMailboxAddress(authenticated);
    if (!user) throw new Error('Credential verifier returned an invalid mailbox');
    return sendJson(res, 200, { authenticated: true, user });
  } catch {
    context.logger.error?.('Dovecot authentication bridge request failed.');
    return sendJson(res, 503, { error: 'Service unavailable.' });
  }
}

function readSharedSecret(filePath) {
  if (!filePath || typeof filePath !== 'string') {
    throw new Error('Dovecot authentication secret file is required');
  }
  const secret = readFileSync(filePath, 'utf8').trim();
  const bytes = Buffer.byteLength(secret, 'utf8');
  if (bytes < 32 || bytes > 512 || /\s/.test(secret)) {
    throw new Error('Dovecot authentication secret must be a 32-512 byte token');
  }
  return secret;
}

function validBearerSecret(header, expectedDigest) {
  const match = String(header || '').match(/^Bearer\s+([^\s]+)$/i);
  const actualDigest = digestSecret(match?.[1] || '');
  return Boolean(match) && crypto.timingSafeEqual(actualDigest, expectedDigest);
}

function digestSecret(value) {
  return crypto.createHash('sha256').update(value).digest();
}

function requestPathname(req) {
  try {
    return new URL(req.url || '/', 'http://mailhub.internal').pathname;
  } catch {
    return '';
  }
}

function requestContentType(req) {
  return String(req.headers['content-type'] || '').split(';', 1)[0].trim().toLowerCase();
}

function parseContentLength(value) {
  if (value === undefined) return 0;
  const raw = String(value);
  if (!/^\d+$/.test(raw)) return null;
  const parsed = Number(raw);
  return Number.isSafeInteger(parsed) ? parsed : null;
}

async function readJson(req, limit) {
  const chunks = [];
  let bytes = 0;
  for await (const chunk of req) {
    bytes += chunk.length;
    if (bytes > limit) throw new RequestTooLargeError();
    chunks.push(chunk);
  }
  if (!chunks.length) throw new Error('Request body is required');
  const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
  if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Object body is required');
  return body;
}

function normalizeAuthRequest(body) {
  if (typeof body.username !== 'string' || typeof body.password !== 'string') return null;
  if (typeof body.remoteIp !== 'string') return null;
  const username = body.username.trim();
  const remoteIp = body.remoteIp.trim();
  const service = String(body.service || 'imap').trim().toLowerCase();
  if (!username || Buffer.byteLength(username, 'utf8') > 320 || /[\r\n\u0000]/.test(username)) return null;
  if (Buffer.byteLength(body.password, 'utf8') > defaultBodyLimit) return null;
  if (!isIP(remoteIp) || !['imap', 'pop3'].includes(service)) return null;
  return { username, password: body.password, remoteIp };
}

function canonicalMailboxAddress(authenticated) {
  const rawAddress = String(authenticated?.mailbox?.address || '');
  const address = rawAddress.toLowerCase();
  if (
    rawAddress !== rawAddress.trim()
    || Buffer.byteLength(address, 'utf8') > 320
    || /[\/\\\u0000\s]/.test(address)
    || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)
  ) return '';
  return address;
}

function setPrivateHeaders(res) {
  res.setHeader('Cache-Control', 'no-store');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('X-Content-Type-Options', 'nosniff');
}

function sendJson(res, status, payload, headers = {}) {
  if (res.writableEnded) return;
  const body = JSON.stringify(payload);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': String(Buffer.byteLength(body)),
    ...headers
  });
  res.end(body);
}

function sendTooLarge(req, res) {
  req.resume();
  return sendJson(res, 413, { error: 'Request too large.' }, { Connection: 'close' });
}

function positiveInteger(value, fallback) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

class RequestTooLargeError extends Error {}
