import crypto from 'node:crypto';
import { readFileSync } from 'node:fs';

import {
  exchangeWebmailLoginTicket,
  revokeWebmailCredential
} from './db.js';

const internalPrefix = '/internal/webmail-sso/';
const bodyLimit = 8 * 1024;

export function loadWebmailSsoConfig(env = process.env) {
  const url = normalizeWebmailUrl(env.WEBMAIL_SSO_URL);
  const secretFile = String(env.WEBMAIL_SSO_SECRET_FILE || '').trim();
  if (!url) {
    return {
      enabled: false,
      url: '',
      audience: '',
      ticketTtlSeconds: 60,
      credentialTtlSeconds: 43_200,
      secretDigest: null
    };
  }
  if (!secretFile) {
    throw new Error('Webmail SSO URL and secret file must be configured together');
  }
  return {
    enabled: true,
    url,
    audience: new URL(url).origin,
    ticketTtlSeconds: boundedInteger(env.WEBMAIL_SSO_TICKET_TTL_SECONDS, 60, 10, 300),
    credentialTtlSeconds: boundedInteger(env.WEBMAIL_SSO_CREDENTIAL_TTL_SECONDS, 43_200, 300, 86_400),
    secretDigest: digestSecret(readSharedSecret(secretFile))
  };
}

export async function handleInternalWebmailSso(req, res, url, config, dependencies = {}) {
  if (!url.pathname.startsWith(internalPrefix)) return false;
  setPrivateHeaders(res);
  if (!config.enabled) {
    sendJson(res, 503, { error: 'Webmail SSO is unavailable.' });
    return true;
  }
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    sendJson(res, 405, { error: 'Method not allowed.' });
    return true;
  }
  if (!validBearerSecret(req.headers.authorization, config.secretDigest)) {
    sendJson(res, 401, { error: 'Unauthorized.' });
    return true;
  }
  if (requestContentType(req) !== 'application/json') {
    sendJson(res, 415, { error: 'Unsupported media type.' });
    return true;
  }

  let body;
  try {
    body = await readJson(req, bodyLimit);
  } catch (error) {
    sendJson(res, error instanceof RequestTooLargeError ? 413 : 400, {
      error: error instanceof RequestTooLargeError ? 'Request too large.' : 'Invalid request.'
    });
    return true;
  }
  if (body.audience !== config.audience) {
    sendJson(res, 400, { error: 'Invalid request.' });
    return true;
  }

  const exchange = dependencies.exchangeWebmailLoginTicket || exchangeWebmailLoginTicket;
  const revoke = dependencies.revokeWebmailCredential || revokeWebmailCredential;
  if (url.pathname === `${internalPrefix}exchange`) {
    const ticket = normalizeOpaqueCredential(body.ticket, 'mht_');
    if (!ticket) {
      sendJson(res, 400, { error: 'Invalid or expired login.' });
      return true;
    }
    const result = exchange(ticket, { audience: config.audience });
    if (!result) {
      sendJson(res, 400, { error: 'Invalid or expired login.' });
      return true;
    }
    sendJson(res, 200, {
      username: result.mailbox.address,
      credential: result.credential,
      expiresAt: result.expiresAt
    });
    return true;
  }

  if (url.pathname === `${internalPrefix}revoke`) {
    const credential = normalizeOpaqueCredential(body.credential, 'mhw_');
    if (!credential) {
      sendJson(res, 400, { error: 'Invalid request.' });
      return true;
    }
    revoke(credential, { audience: config.audience });
    sendJson(res, 200, { revoked: true });
    return true;
  }

  sendJson(res, 404, { error: 'Not found.' });
  return true;
}

export function isSameOriginRequest(req, appBaseUrl, { trustProxy = false } = {}) {
  const suppliedOrigin = normalizeOrigin(req.headers.origin);
  if (!suppliedOrigin) return false;
  const allowed = new Set();
  const configuredOrigin = normalizeOrigin(appBaseUrl);
  if (configuredOrigin) allowed.add(configuredOrigin);
  const requestOrigin = externalRequestOrigin(req, trustProxy);
  if (requestOrigin) allowed.add(requestOrigin);
  return allowed.has(suppliedOrigin);
}

function externalRequestOrigin(req, trustProxy) {
  const forwardedHost = trustProxy ? firstForwardedValue(req.headers['x-forwarded-host']) : '';
  const host = String(forwardedHost || req.headers.host || '').trim();
  if (!host || /[\s/\\]/.test(host)) return '';
  const forwardedProto = trustProxy ? firstForwardedValue(req.headers['x-forwarded-proto']) : '';
  const protocol = String(forwardedProto || (req.socket?.encrypted ? 'https' : 'http')).toLowerCase();
  if (!['http', 'https'].includes(protocol)) return '';
  return normalizeOrigin(`${protocol}://${host}`);
}

function firstForwardedValue(value) {
  return String(value || '').split(',', 1)[0].trim();
}

function normalizeWebmailUrl(value) {
  const raw = String(value || '').trim();
  if (!raw) return '';
  let parsed;
  try {
    parsed = new URL(raw);
  } catch {
    throw new Error('Webmail SSO URL is invalid');
  }
  const localDevelopment = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
  if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && localDevelopment)) {
    throw new Error('Webmail SSO URL must use HTTPS');
  }
  if (parsed.username || parsed.password) throw new Error('Webmail SSO URL must not contain credentials');
  parsed.search = '';
  parsed.hash = '';
  return parsed.toString();
}

function normalizeOrigin(value) {
  try {
    const parsed = new URL(String(value || '').trim());
    if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) return '';
    return parsed.origin;
  } catch {
    return '';
  }
}

function normalizeOpaqueCredential(value, prefix) {
  const clean = String(value || '');
  if (!clean.startsWith(prefix) || !/^[A-Za-z0-9_-]+$/.test(clean)) return '';
  if (clean.length < 32 || clean.length > 256) return '';
  return clean;
}

function readSharedSecret(filePath) {
  const secret = readFileSync(filePath, 'utf8').trim();
  if (!/^[0-9a-fA-F]{64,512}$/.test(secret)) {
    throw new Error('Webmail SSO secret must contain 64-512 hexadecimal characters');
  }
  return secret;
}

function validBearerSecret(header, expectedDigest) {
  if (!expectedDigest) return false;
  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 requestContentType(req) {
  return String(req.headers['content-type'] || '').split(';', 1)[0].trim().toLowerCase();
}

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 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) {
  const body = JSON.stringify(payload);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': String(Buffer.byteLength(body))
  });
  res.end(body);
}

function boundedInteger(value, fallback, minimum, maximum) {
  const parsed = Number(value ?? fallback);
  if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
    throw new Error(`Webmail SSO TTL must be between ${minimum} and ${maximum} seconds`);
  }
  return parsed;
}

class RequestTooLargeError extends Error {}
