import { FastifyReply, FastifyRequest } from 'fastify';
import { getRedis, REDIS_KEYS } from '../redis';
import { config } from '../config';
import { extractClientIp } from '../geo';

const BLOCKED_SAMPLE_TTL_SEC = 60 * 60 * 24 * 14;

const SCANNER_PATH_PATTERNS: RegExp[] = [
  /^\/(?:wp-login\.php|xmlrpc\.php)(?:[/?#]|$)/i,
  /^\/wp-admin(?:[/?#]|$)/i,
  /^\/(?:phpmyadmin|phpMyAdmin|pma|myadmin|mysqladmin)(?:[/?#]|$)/i,
  /^\/admin\/login\.asp(?:[/?#]|$)/i,
  /^\/boaform\/admin\/formlogin(?:[/?#]|$)/i,
  /^\/cgi-bin(?:[/?#]|$)/i,
  /^\/(?:\.env|\.env\.[^/]+)(?:[/?#]|$)/i,
  /^\/(?:\.git|\.svn|\.hg)(?:[/?#]|$)/i,
  /^\/(?:config|configuration|settings|database|db|wp-config)\.php(?:[/?#]|$)/i,
  /^\/vendor\/phpunit(?:[/?#]|$)/i,
  /^\/(?:phpunit|eval-stdin\.php)(?:[/?#]|$)/i,
  /^\/(?:setup|install|installer|upgrade)\.php(?:[/?#]|$)/i,
  /^\/(?:login\.action|act-guest-login)(?:[/?#]|$)/i,
  /^\/(?:hudson|jenkins|solr|manager\/html|server-status)(?:[/?#]|$)/i,
  /^\/(?:HNAP1|GponForm|goform|luci|remote\/login|sdk)(?:[/?#]|$)/i,
  /^\/(?:etc\/passwd|passwd)(?:[/?#]|$)/i,
  /^\/(?:shell|cmd|webshell|wso|c99|r57)\.php(?:[/?#]|$)/i,
  /\.(?:php|asp|aspx|jsp|cgi)(?:[/?#]|$)/i,
  /(?:\.\.\/|\.\.%2f|%2e%2e%2f)/i,
];

const SUSPICIOUS_UA_PATTERNS: RegExp[] = [
  /(?:acunetix|ahrefsbot|apachebench|arachni|bbbike|blexbot|burp|crawler4j|dirbuster)/i,
  /(?:gobuster|masscan|nessus|nikto|nmap|openvas|python-requests|sqlmap|wpscan|zgrab|zmeu)/i,
  /(?:go-http-client|libwww-perl|metasploit|nuclei|petalbot|semrushbot|whatweb)/i,
];

export async function scannerBlocker(
  request: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  const path = request.url.split('?')[0] || '/';
  if (isExemptPath(path)) return;

  const userAgent = String(request.headers['user-agent'] ?? '');
  const reason = getBlockReason(path, userAgent);
  if (!reason) return;

  await recordBlockedRequest(request, path, reason);
  reply.status(404).type('text/plain').send('Not Found');
}

function getBlockReason(path: string, userAgent: string): string | null {
  const decodedPath = safeDecode(path);
  const matchedPath = SCANNER_PATH_PATTERNS.find(
    (pattern) => pattern.test(path) || pattern.test(decodedPath),
  );
  if (matchedPath) return `path:${matchedPath.source.slice(0, 80)}`;

  const matchedUa = SUSPICIOUS_UA_PATTERNS.find((pattern) => pattern.test(userAgent));
  if (matchedUa) return `ua:${matchedUa.source.slice(0, 80)}`;

  return null;
}

function isExemptPath(path: string): boolean {
  return (
    path === '/analytics.js' ||
    path.startsWith(config.analytics.dashboardPath) ||
    path.startsWith(config.analytics.apiPrefix) ||
    path.startsWith(config.admin.path) ||
    path.startsWith(config.intercept.pingPath) ||
    path.startsWith(config.intercept.cookieDeclarationPath) ||
    path.startsWith(config.intercept.googleMapsBridgePath) ||
    path.startsWith(config.intercept.googleMapsSwPath) ||
    path.startsWith(config.intercept.googleMapsProxyPrefix) ||
    path.startsWith(config.intercept.luigisboxProxyPrefix) ||
    path.startsWith(config.intercept.wishlistApiProxyPrefix) ||
    path.startsWith(config.cdnPrefix) ||
    path.startsWith(config.staticCdnPrefix) ||
    path.startsWith('/api/') ||
    path.startsWith('/orderapi') ||
    path.startsWith('/checkout') ||
    path.startsWith('/__preview/') ||
    path.startsWith('/pl/')
  );
}

async function recordBlockedRequest(
  request: FastifyRequest,
  path: string,
  reason: string,
): Promise<void> {
  const redis = getRedis();
  const day = new Date().toISOString().slice(0, 10);
  const countKey = REDIS_KEYS.securityBlockedCount(day);
  const samplesKey = REDIS_KEYS.securityBlockedSamples(day);
  const sample = {
    at: new Date().toISOString(),
    ip: extractClientIp(request),
    method: request.method,
    path: path.slice(0, 220),
    ua: String(request.headers['user-agent'] ?? '').slice(0, 180),
    reason,
  };

  try {
    await redis
      .multi()
      .incr(countKey)
      .expire(countKey, BLOCKED_SAMPLE_TTL_SEC)
      .lpush(samplesKey, JSON.stringify(sample))
      .ltrim(samplesKey, 0, 49)
      .expire(samplesKey, BLOCKED_SAMPLE_TTL_SEC)
      .exec();
  } catch (err) {
    console.error('[security] blocked counter error:', (err as Error).message);
  }

  if (process.env.SECURITY_DEBUG === 'true') {
    console.log('[security] blocked scanner', sample);
  }
}

function safeDecode(path: string): string {
  try {
    return decodeURIComponent(path);
  } catch {
    return path;
  }
}
