const crypto = require('crypto');
const {
  envOrConfigBool,
  envOrConfigList,
  envOrConfigNumber,
  getNetworkConfig,
  getSecurityConfig,
} = require('./config');

const SECURITY_CONFIG = getSecurityConfig().security || {};
const PAYLOAD_LIMITS = getSecurityConfig().payload_limits || {};
const NETWORK_LIMITS = getNetworkConfig().limits || {};

const DEFAULT_API_MAX_BODY_BYTES = Math.max(
  4096,
  envOrConfigNumber('MAZEX_API_MAX_BODY_BYTES', PAYLOAD_LIMITS.max_json_bytes, 512 * 1024, {
    min: 4096,
    integer: true,
  }),
);
const DEFAULT_API_RATE_WINDOW_MS = Math.max(
  1000,
  envOrConfigNumber('MAZEX_API_RATE_WINDOW_MS', NETWORK_LIMITS.rate_window_ms, 60_000, {
    min: 1000,
    integer: true,
  }),
);
const DEFAULT_API_RATE_LIMIT = Math.max(1, Math.trunc(Number(process.env.MAZEX_API_RATE_LIMIT || 600)));
const DEFAULT_API_POST_RATE_LIMIT = Math.max(1, Math.trunc(Number(process.env.MAZEX_API_POST_RATE_LIMIT || 180)));
const DEFAULT_API_CRITICAL_RATE_LIMIT = Math.max(
  1,
  Math.trunc(Number(process.env.MAZEX_API_CRITICAL_RATE_LIMIT || 60)),
);
const DEFAULT_API_NETWORK_RATE_LIMIT = Math.max(
  1,
  Math.trunc(Number(process.env.MAZEX_API_NETWORK_RATE_LIMIT || 240)),
);
const DEFAULT_API_MAX_PATH_LENGTH = Math.max(128, Math.trunc(Number(process.env.MAZEX_API_MAX_PATH_LENGTH || 2048)));
const DEFAULT_API_MAX_JSON_DEPTH = Math.max(4, Math.trunc(Number(process.env.MAZEX_API_MAX_JSON_DEPTH || 24)));
const DEFAULT_API_MAX_JSON_KEYS = Math.max(32, Math.trunc(Number(process.env.MAZEX_API_MAX_JSON_KEYS || 4096)));
const DEFAULT_API_MAX_ARRAY_LENGTH = Math.max(16, Math.trunc(Number(process.env.MAZEX_API_MAX_ARRAY_LENGTH || 2048)));
const DEFAULT_API_MAX_STRING_BYTES = Math.max(
  1024,
  Math.trunc(Number(process.env.MAZEX_API_MAX_STRING_BYTES || 128 * 1024)),
);

const TRUST_PROXY = envOrConfigBool('MAZEX_TRUST_PROXY', SECURITY_CONFIG.trust_proxy, false);
const ENABLE_HSTS = envOrConfigBool('MAZEX_ENABLE_HSTS', SECURITY_CONFIG.enable_hsts, false);
const ENABLE_REMOTE_WALLET_API = envOrConfigBool(
  'MAZEX_ENABLE_REMOTE_WALLET_API',
  SECURITY_CONFIG.enable_remote_wallet_api,
  false,
);
const ADMIN_TOKEN = String(process.env.MAZEX_ADMIN_TOKEN || '').trim();
const ALLOWED_ORIGINS = envOrConfigList('MAZEX_API_ALLOWED_ORIGINS', SECURITY_CONFIG.allowed_origins, []);

const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'DELETE', 'OPTIONS']);
const FORBIDDEN_JSON_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
const buckets = new Map();

function nowMs() {
  return Date.now();
}

function ensureRequestId(req) {
  if (!req.mazexRequestId) {
    req.mazexRequestId = crypto.randomBytes(12).toString('hex');
  }
  return req.mazexRequestId;
}

function normalizeIp(raw) {
  const value = String(raw || '').trim();
  if (!value) return 'unknown';
  if (value === '::1') return '127.0.0.1';
  if (value.startsWith('::ffff:')) return value.slice('::ffff:'.length);
  return value;
}

function clientIp(req) {
  if (TRUST_PROXY) {
    const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
    if (forwarded) return normalizeIp(forwarded);
  }
  return normalizeIp(req.socket?.remoteAddress || 'unknown');
}

function isLoopbackIp(ip) {
  const value = normalizeIp(ip);
  return (
    value === '127.0.0.1' ||
    value === 'localhost' ||
    value === '::1' ||
    value.startsWith('127.') ||
    value === '0:0:0:0:0:0:0:1'
  );
}

function cleanupBuckets(now = nowMs()) {
  for (const [key, bucket] of buckets.entries()) {
    if (bucket.reset_at <= now) buckets.delete(key);
  }
}

function takeRateToken(key, limit, windowMs) {
  const now = nowMs();
  cleanupBuckets(now);
  const bucket = buckets.get(key) || {
    count: 0,
    reset_at: now + windowMs,
  };
  if (bucket.reset_at <= now) {
    bucket.count = 0;
    bucket.reset_at = now + windowMs;
  }
  bucket.count += 1;
  buckets.set(key, bucket);
  if (bucket.count > limit) {
    return {
      ok: false,
      retry_after_ms: Math.max(0, bucket.reset_at - now),
      count: bucket.count,
      limit,
    };
  }
  return {
    ok: true,
    retry_after_ms: Math.max(0, bucket.reset_at - now),
    count: bucket.count,
    limit,
  };
}

function requestUrl(req) {
  try {
    return new URL(String(req.url || '/'), `http://${req.headers.host || 'localhost'}`);
  } catch {
    return null;
  }
}

function hasJsonBody(req) {
  return Number(req.headers['content-length'] || 0) > 0 || Boolean(req.headers['transfer-encoding']);
}

function isJsonContentType(req) {
  const contentType = String(req.headers['content-type'] || '').toLowerCase();
  if (!contentType) return false;
  const mediaType = contentType.split(';')[0].trim();
  return mediaType === 'application/json' || mediaType.endsWith('+json');
}

function isSameOrigin(req, origin) {
  if (!origin) return true;
  try {
    const parsed = new URL(origin);
    const host = String(req.headers.host || '').toLowerCase();
    return parsed.host.toLowerCase() === host && (parsed.protocol === 'http:' || parsed.protocol === 'https:');
  } catch {
    return false;
  }
}

function isAllowedOrigin(req, origin) {
  if (!origin) return true;
  if (isSameOrigin(req, origin)) return true;
  if (ALLOWED_ORIGINS.includes('*')) return true;
  return ALLOWED_ORIGINS.includes(origin);
}

function routeRateClass(pathname, method) {
  if (SAFE_METHODS.has(method)) return null;
  if (
    pathname.includes('/submit') ||
    pathname.includes('/transfer') ||
    pathname.includes('/sign') ||
    pathname.includes('/reset') ||
    pathname.includes('/admin')
  ) {
    return { name: 'critical', limit: DEFAULT_API_CRITICAL_RATE_LIMIT };
  }
  if (pathname.includes('/node/') || pathname.includes('/p2p/') || pathname.includes('/chain/blocks')) {
    return { name: 'network', limit: DEFAULT_API_NETWORK_RATE_LIMIT };
  }
  return { name: 'write', limit: DEFAULT_API_POST_RATE_LIMIT };
}

function reject(status, code, message, extra = {}) {
  return {
    ok: false,
    status,
    code,
    message,
    ...extra,
  };
}

function checkPathSafety(rawPath, pathname) {
  if (rawPath.length > DEFAULT_API_MAX_PATH_LENGTH) {
    return reject(414, 'URI_TOO_LONG', 'Request path exceeds mazexchain mainnet safety limit', {
      max_path_length: DEFAULT_API_MAX_PATH_LENGTH,
    });
  }
  if (rawPath.includes('\0') || /%00/i.test(rawPath)) {
    return reject(400, 'INVALID_PATH', 'Request path contains a null byte');
  }
  if (rawPath.includes('\\')) {
    return reject(400, 'INVALID_PATH', 'Request path contains an invalid separator');
  }
  let decoded = pathname;
  try {
    decoded = decodeURIComponent(pathname);
  } catch {
    return reject(400, 'INVALID_PATH_ENCODING', 'Request path must be valid UTF-8 percent encoding');
  }
  const parts = decoded.split('/').filter(Boolean);
  if (parts.includes('..')) {
    return reject(403, 'PATH_TRAVERSAL_REJECTED', 'Path traversal is not allowed');
  }
  return { ok: true };
}

function checkAdminBoundary(req, pathname, ip) {
  const isAdminPath = pathname === '/api/base1/reset';
  if (!isAdminPath) return { ok: true };
  if (ADMIN_TOKEN) {
    const token = String(req.headers['x-mazex-admin-token'] || req.headers.authorization || '').replace(/^Bearer\s+/i, '');
    if (token !== ADMIN_TOKEN) {
      return reject(403, 'ADMIN_TOKEN_REQUIRED', 'Admin API requires a valid mazexchain admin token');
    }
    return { ok: true };
  }
  if (!isLoopbackIp(ip)) {
    return reject(403, 'ADMIN_API_LOOPBACK_ONLY', 'Admin API is loopback-only unless MAZEX_ADMIN_TOKEN is configured');
  }
  return { ok: true };
}

function checkWalletBoundary(pathname, ip) {
  if (!pathname.startsWith('/api/wallet/')) return { ok: true };
  if (ENABLE_REMOTE_WALLET_API || isLoopbackIp(ip)) return { ok: true };
  return reject(403, 'WALLET_API_LOOPBACK_ONLY', 'Wallet private-key API is loopback-only');
}

function checkHttpRequest(req) {
  ensureRequestId(req);
  const method = String(req.method || '').toUpperCase();
  if (!ALLOWED_METHODS.has(method)) {
    return reject(405, 'METHOD_NOT_ALLOWED', 'HTTP method is not allowed by mazexchain API policy');
  }

  const url = requestUrl(req);
  if (!url) return reject(400, 'INVALID_URL', 'Request URL is invalid');

  const pathCheck = checkPathSafety(String(req.url || ''), url.pathname);
  if (!pathCheck.ok) return pathCheck;

  const origin = String(req.headers.origin || '').trim();
  if (!SAFE_METHODS.has(method) && origin && !isAllowedOrigin(req, origin)) {
    return reject(403, 'ORIGIN_NOT_ALLOWED', 'Request origin is not allowed by mazexchain API policy', {
      origin,
    });
  }

  if (!SAFE_METHODS.has(method) && hasJsonBody(req) && !isJsonContentType(req)) {
    return reject(415, 'UNSUPPORTED_MEDIA_TYPE', 'Mutating API requests with a body must use application/json');
  }

  const ip = clientIp(req);
  const admin = checkAdminBoundary(req, url.pathname, ip);
  if (!admin.ok) return admin;
  const wallet = checkWalletBoundary(url.pathname, ip);
  if (!wallet.ok) return wallet;

  const windowMs = DEFAULT_API_RATE_WINDOW_MS;
  const base = takeRateToken(`all:${ip}`, DEFAULT_API_RATE_LIMIT, windowMs);
  if (!base.ok) {
    return reject(429, 'RATE_LIMITED', 'Too many requests from this peer', {
      retry_after_ms: base.retry_after_ms,
      limit: base.limit,
    });
  }

  const rateClass = routeRateClass(url.pathname, method);
  if (rateClass) {
    const scoped = takeRateToken(`${rateClass.name}:${ip}`, rateClass.limit, windowMs);
    if (!scoped.ok) {
      return reject(429, `${rateClass.name.toUpperCase()}_RATE_LIMITED`, 'Too many write requests from this peer', {
        retry_after_ms: scoped.retry_after_ms,
        limit: scoped.limit,
      });
    }
    const contentLength = Number(req.headers['content-length'] || 0);
    if (contentLength > DEFAULT_API_MAX_BODY_BYTES) {
      return reject(413, 'BODY_TOO_LARGE', 'Request body exceeds mazexchain mainnet safety limit', {
        max_body_bytes: DEFAULT_API_MAX_BODY_BYTES,
        content_length: contentLength,
      });
    }
  }
  return { ok: true, request_id: req.mazexRequestId };
}

function validateJsonValue(value, stats, depth = 0, path = '$') {
  if (depth > DEFAULT_API_MAX_JSON_DEPTH) {
    return reject(400, 'JSON_TOO_DEEP', 'JSON body exceeds maximum nesting depth', {
      max_depth: DEFAULT_API_MAX_JSON_DEPTH,
      path,
    });
  }
  if (value === null) return { ok: true };
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) return reject(400, 'INVALID_JSON_NUMBER', 'JSON number must be finite', { path });
    return { ok: true };
  }
  if (typeof value === 'string') {
    const bytes = Buffer.byteLength(value, 'utf8');
    if (bytes > DEFAULT_API_MAX_STRING_BYTES) {
      return reject(413, 'JSON_STRING_TOO_LARGE', 'JSON string exceeds maximum byte length', {
        max_string_bytes: DEFAULT_API_MAX_STRING_BYTES,
        string_bytes: bytes,
        path,
      });
    }
    return { ok: true };
  }
  if (typeof value === 'boolean') return { ok: true };
  if (Array.isArray(value)) {
    if (value.length > DEFAULT_API_MAX_ARRAY_LENGTH) {
      return reject(413, 'JSON_ARRAY_TOO_LARGE', 'JSON array exceeds maximum length', {
        max_array_length: DEFAULT_API_MAX_ARRAY_LENGTH,
        array_length: value.length,
        path,
      });
    }
    for (let index = 0; index < value.length; index += 1) {
      const child = validateJsonValue(value[index], stats, depth + 1, `${path}[${index}]`);
      if (!child.ok) return child;
    }
    return { ok: true };
  }
  if (typeof value === 'object') {
    const keys = Object.keys(value);
    stats.keys += keys.length;
    if (stats.keys > DEFAULT_API_MAX_JSON_KEYS) {
      return reject(413, 'JSON_TOO_MANY_KEYS', 'JSON body exceeds maximum key count', {
        max_keys: DEFAULT_API_MAX_JSON_KEYS,
        key_count: stats.keys,
      });
    }
    for (const key of keys) {
      if (FORBIDDEN_JSON_KEYS.has(key)) {
        return reject(400, 'JSON_FORBIDDEN_KEY', 'JSON body contains a forbidden object key', {
          key,
          path,
        });
      }
      const child = validateJsonValue(value[key], stats, depth + 1, `${path}.${key}`);
      if (!child.ok) return child;
    }
    return { ok: true };
  }
  return reject(400, 'INVALID_JSON_VALUE', 'JSON body contains an unsupported value type', { path });
}

function readJsonBody(req, { maxBytes = DEFAULT_API_MAX_BODY_BYTES } = {}) {
  return new Promise((resolve, rejectPromise) => {
    let raw = '';
    let size = 0;
    let settled = false;

    function fail(error) {
      if (settled) return;
      settled = true;
      rejectPromise(error);
    }

    req.on('data', (chunk) => {
      size += chunk.length;
      if (size > maxBytes) {
        const error = new Error('Request body exceeds mazexchain mainnet safety limit');
        error.code = 'BODY_TOO_LARGE';
        error.status = 413;
        fail(error);
        req.destroy(error);
        return;
      }
      raw += chunk.toString('utf8');
    });
    req.on('end', () => {
      if (settled) return;
      if (!raw) {
        settled = true;
        resolve({});
        return;
      }
      try {
        const parsed = JSON.parse(raw);
        const shape = validateJsonValue(parsed, { keys: 0 });
        if (!shape.ok) {
          const error = new Error(shape.message);
          error.code = shape.code;
          error.status = shape.status;
          error.detail = shape;
          fail(error);
          return;
        }
        settled = true;
        resolve(parsed);
      } catch (parseError) {
        if (parseError.code) return fail(parseError);
        const error = new Error('Request body must be valid JSON');
        error.code = 'INVALID_JSON';
        error.status = 400;
        fail(error);
      }
    });
    req.on('error', fail);
  });
}

function corsHeaders(req) {
  const origin = String(req?.headers?.origin || '').trim();
  if (!origin || !isAllowedOrigin(req, origin)) return {};
  return {
    'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes('*') ? '*' : origin,
    'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-MazeX-Admin-Token',
    'Access-Control-Max-Age': '600',
    Vary: 'Origin',
  };
}

function securityHeaders(req = null) {
  const headers = {
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'X-Permitted-Cross-Domain-Policies': 'none',
    'X-Download-Options': 'noopen',
    'Referrer-Policy': 'no-referrer',
    'Permissions-Policy':
      'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), xr-spatial-tracking=()',
    'Cross-Origin-Opener-Policy': 'same-origin',
    'Cross-Origin-Resource-Policy': 'same-origin',
    'Content-Security-Policy':
      "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
    ...corsHeaders(req),
  };
  if (req?.mazexRequestId) headers['X-Request-Id'] = req.mazexRequestId;
  if (ENABLE_HSTS) headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains; preload';
  return headers;
}

function safeErrorBody(error, req = null) {
  const status = Math.max(400, Math.trunc(Number(error?.status || error?.statusCode || 400)));
  const code = String(error?.code || (status >= 500 ? 'INTERNAL_ERROR' : 'BAD_REQUEST'));
  const detail = error?.detail && typeof error.detail === 'object' ? error.detail : null;
  const message =
    status >= 500
      ? 'Internal mazexchain service error'
      : String(error?.message || detail?.message || 'Request rejected by mazexchain API policy');
  return {
    ok: false,
    code,
    message,
    request_id: req?.mazexRequestId || null,
    ...(detail && detail.code ? { detail } : {}),
  };
}

function getMainnetSecurityStatus() {
  return {
    api_max_body_bytes: DEFAULT_API_MAX_BODY_BYTES,
    api_rate_window_ms: DEFAULT_API_RATE_WINDOW_MS,
    api_rate_limit: DEFAULT_API_RATE_LIMIT,
    api_post_rate_limit: DEFAULT_API_POST_RATE_LIMIT,
    api_critical_rate_limit: DEFAULT_API_CRITICAL_RATE_LIMIT,
    api_network_rate_limit: DEFAULT_API_NETWORK_RATE_LIMIT,
    api_max_path_length: DEFAULT_API_MAX_PATH_LENGTH,
    api_max_json_depth: DEFAULT_API_MAX_JSON_DEPTH,
    api_max_json_keys: DEFAULT_API_MAX_JSON_KEYS,
    api_max_array_length: DEFAULT_API_MAX_ARRAY_LENGTH,
    api_max_string_bytes: DEFAULT_API_MAX_STRING_BYTES,
    trust_proxy: TRUST_PROXY,
    hsts_enabled: ENABLE_HSTS,
    allowed_origin_count: ALLOWED_ORIGINS.length,
    remote_wallet_api_enabled: ENABLE_REMOTE_WALLET_API,
    admin_token_configured: Boolean(ADMIN_TOKEN),
    active_rate_buckets: buckets.size,
  };
}

module.exports = {
  checkHttpRequest,
  clientIp,
  ensureRequestId,
  getMainnetSecurityStatus,
  readJsonBody,
  safeErrorBody,
  securityHeaders,
};
