import crypto from 'node:crypto';

import { safeEqual } from '../libs/auth.js';

const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
const usedNonces = new Map();

const normalizePath = (path) => {
  if (!path || path === '/') {
    return '/';
  }
  return path.endsWith('/') ? path.slice(0, -1) : path;
};

const stableStringify = (value) => {
  if (Array.isArray(value)) {
    return `[${value.map(stableStringify).join(',')}]`;
  }

  if (value && typeof value === 'object') {
    return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
  }

  return JSON.stringify(value);
};

const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');

const getUsers = () => {
  return process.env.PINNACLE_PROXY_USERS?.split(';')
  .map(item => item.trim())
  .filter(Boolean)
  .map(item => {
    const [username, secret, permissions=''] = item.split(':');
    return {
      permissions: permissions.split(',').map(permission => normalizePath(permission.trim())).filter(Boolean),
      secret,
      username,
    };
  })
  .filter(user => user.username && user.secret) ?? [];
};

const getUser = (username) => getUsers().find(user => safeEqual(user.username, username));

const cleanupUsedNonces = () => {
  const now = Date.now();
  usedNonces.forEach((expiresAt, key) => {
    if (expiresAt <= now) {
      usedNonces.delete(key);
    }
  });
};

const checkNonce = (username, nonce) => {
  cleanupUsedNonces();

  const key = `${username}:${nonce}`;
  if (usedNonces.has(key)) {
    return false;
  }

  usedNonces.set(key, Date.now() + MAX_CLOCK_SKEW_MS);
  return true;
};

const hasPermission = (user, path) => {
  const targetPath = normalizePath(path);
  return user.permissions.includes('*') || user.permissions.includes(targetPath);
};

const getBodyHash = (body) => {
  if (body === undefined || body === null || Object.keys(body).length === 0) {
    return sha256('');
  }
  return sha256(stableStringify(body));
};

const buildSigningPayload = (req, timestamp, nonce) => [
  req.method.toUpperCase(),
  normalizePath(req.path),
  stableStringify(req.query ?? {}),
  getBodyHash(req.body),
  timestamp,
  nonce,
].join('\n');

const requirePinnacleProxyAuth = (req, res, next) => {
  const username = req.get('x-api-user');
  const timestamp = req.get('x-api-timestamp');
  const nonce = req.get('x-api-nonce');
  const signature = req.get('x-api-signature');

  if (!username || !timestamp || !nonce || !signature) {
    return res.unauthorized('Missing API signature headers');
  }

  const requestTime = Number(timestamp);
  if (!Number.isFinite(requestTime) || Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
    return res.unauthorized('Invalid API timestamp');
  }

  const user = getUser(username);
  if (!user) {
    return res.unauthorized('Invalid API user');
  }

  if (!hasPermission(user, req.path)) {
    return res.status(403).json({ statusCode: 403, code: -1, message: 'Forbidden' });
  }

  const payload = buildSigningPayload(req, timestamp, nonce);
  const expectedSignature = crypto.createHmac('sha256', user.secret).update(payload).digest('hex');

  if (!safeEqual(signature, expectedSignature)) {
    return res.unauthorized('Invalid API signature');
  }

  if (!checkNonce(username, nonce)) {
    return res.unauthorized('API nonce has already been used');
  }

  req.pinnacleProxyUser = { username };
  return next();
};

export default requirePinnacleProxyAuth;
