import { createServer, IncomingMessage, ServerResponse } from "node:http";
import { promises as fs } from "node:fs";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import {
  gzipSync,
  brotliCompressSync,
  constants as zlibConstants,
} from "node:zlib";
import path from "node:path";
import ejs from "ejs";

const publicDir = path.join(__dirname, "public");
const viewsDir = path.join(__dirname, "views");
// Compresión síncrona (rápida para tamaños pequeños de HTML/CSS/JS)

// Cargar metadatos del proyecto para usarlos como variables en las vistas
let pkg: {
  name?: string;
  version?: string;
  dependencies?: Record<string, string>;
} = {};
try {
  const pkgPath = path.join(__dirname, "../../package.json");
  pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
} catch {
  // Ignorar si no se puede leer; usaremos valores por defecto
}

const MIME_TYPES: Record<string, string> = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".svg": "image/svg+xml",
  ".webp": "image/webp",
  ".ico": "image/x-icon",
  ".woff": "font/woff",
  ".woff2": "font/woff2",
};

const PORT = Number(process.env.PORT) || 3000;

// --- Basic hardening: blocklist patterns and lightweight rate limiting for suspicious paths ---
const BLOCKED_PATTERNS: RegExp[] = [
  /\b(npci|upi|bhim|aadhaar|aadhar|cts|fastag|bbps|rgcs|nuup|apbs|hdfc|ergo|securities|banking|insurance)\b/i,
];

const SUSP_LENGTH = 18; // long single-segment slugs tend to be bot probes
const RATE_WINDOW_MS = 60_000; // 1 minute window
const RATE_MAX_SUSPICIOUS = 20; // allow up to 20 suspicious hits per minute per IP

type Counter = { count: number; resetAt: number };
const suspiciousCounters = new Map<string, Counter>();

function getClientIp(req: IncomingMessage): string {
  const cf = (req.headers["cf-connecting-ip"] as string) || "";
  const xff = (req.headers["x-forwarded-for"] as string) || "";
  const chain = (cf || xff)
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
  return (
    chain[0] || (req.socket && (req.socket as any).remoteAddress) || "unknown"
  );
}

function isSingleSegment(pathname: string) {
  // e.g. /foo-bar, no additional slashes
  return /^\/[A-Za-z0-9._-]+$/.test(pathname);
}

function isSuspiciousPath(pathname: string): boolean {
  if (
    !pathname ||
    pathname === "/" ||
    pathname === "/index" ||
    pathname === "/index.html"
  )
    return false;
  if (BLOCKED_PATTERNS.some((re) => re.test(pathname))) return true;
  if (isSingleSegment(pathname) && pathname.length > SUSP_LENGTH) return true;
  return false;
}

function hitSuspicious(ip: string): {
  allowed: boolean;
  resetIn: number;
  remaining: number;
} {
  const now = Date.now();
  let bucket = suspiciousCounters.get(ip);
  if (!bucket || now >= bucket.resetAt) {
    bucket = { count: 0, resetAt: now + RATE_WINDOW_MS };
  }
  bucket.count += 1;
  suspiciousCounters.set(ip, bucket);
  const allowed = bucket.count <= RATE_MAX_SUSPICIOUS;
  return {
    allowed,
    resetIn: Math.max(0, bucket.resetAt - now),
    remaining: Math.max(0, RATE_MAX_SUSPICIOUS - bucket.count),
  };
}

function applySecurityHeaders(base: Record<string, string> = {}) {
  return {
    "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
    "X-Content-Type-Options": "nosniff",
    "Referrer-Policy": "no-referrer",
    "X-Frame-Options": "DENY",
    // Mild CSP to avoid breaking inline styles/scripts already present; adjust as needed
    "Content-Security-Policy":
      "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; font-src 'self' https: data:; frame-src 'self' https://ko-fi.com https://*.ko-fi.com; child-src 'self' https://ko-fi.com https://*.ko-fi.com",
    ...base,
  };
}
function buildBaseCsp(frameAncestors: string = "'self'") {
  // Use a mild CSP; add frame-ancestors dynamically per request.
  return (
    "default-src 'self'; " +
    "img-src 'self' data: https:; " +
    "style-src 'self' 'unsafe-inline' https:; " +
    "script-src 'self' 'unsafe-inline' https:; " +
    "font-src 'self' https: data:; " +
    "frame-src 'self' https://ko-fi.com https://*.ko-fi.com; " +
    "child-src 'self' https://ko-fi.com https://*.ko-fi.com; " +
    `frame-ancestors ${frameAncestors}`
  );
}

function applySecurityHeadersForRequest(
  req: IncomingMessage,
  base: Record<string, string> = {}
) {
  const host = ((req.headers.host as string) || "").toLowerCase();
  const isDocsHost =
    host === "docs.amayo.dev" || host.endsWith(".docs.amayo.dev");

  // Allow embedding only from https://top.gg for docs.amayo.dev; otherwise, self only and keep XFO deny.
  const csp = isDocsHost
    ? buildBaseCsp("'self' https://top.gg")
    : buildBaseCsp("'self'");

  const headers: Record<string, string> = {
    "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
    "X-Content-Type-Options": "nosniff",
    "Referrer-Policy": "no-referrer",
    // X-Frame-Options is omitted for docs.amayo.dev to rely on CSP frame-ancestors allowing only top.gg
    ...(isDocsHost ? {} : { "X-Frame-Options": "DENY" }),
    "Content-Security-Policy": csp,
    ...base,
  };

  return headers;
}

function computeEtag(buf: Buffer): string {
  // Weak ETag derived from content sha1 and length
  const hash = createHash("sha1").update(buf).digest("base64");
  return `W/"${buf.length.toString(16)}-${hash}"`;
}

function acceptsEncoding(req: IncomingMessage, enc: string): boolean {
  const ae = (req.headers["accept-encoding"] as string) || "";
  return ae
    .split(",")
    .map((s) => s.trim())
    .includes(enc);
}

// Parse Accept-Encoding with q-values and return a map of encoding -> q
function parseAcceptEncoding(req: IncomingMessage): Map<string, number> {
  const header = (req.headers["accept-encoding"] as string) || "";
  const map = new Map<string, number>();
  if (!header) {
    // If header missing, identity is acceptable
    map.set("identity", 1);
    return map;
  }
  const parts = header.split(",");
  for (const raw of parts) {
    const part = raw.trim();
    if (!part) continue;
    const [name, ...params] = part.split(";");
    let q = 1;
    for (const p of params) {
      const [k, v] = p.split("=").map((s) => s.trim());
      if (k === "q" && v) {
        const n = Number(v);
        if (!Number.isNaN(n)) q = n;
      }
    }
    map.set(name.toLowerCase(), q);
  }
  // Ensure identity exists unless explicitly disabled (q=0)
  if (!map.has("identity")) map.set("identity", 1);
  return map;
}

// Choose the best compression given the request and mime type.
function pickEncoding(
  req: IncomingMessage,
  mime: string
): "br" | "gzip" | "identity" {
  if (!shouldCompress(mime)) return "identity";
  const encs = parseAcceptEncoding(req);
  const qBr = encs.get("br") ?? 0;
  const qGzip = encs.get("gzip") ?? 0;
  if (qBr > 0) return "br";
  if (qGzip > 0) return "gzip";
  return "identity";
}

function shouldCompress(mime: string): boolean {
  return (
    mime.startsWith("text/") ||
    mime.includes("json") ||
    mime.includes("javascript") ||
    mime.includes("svg")
  );
}

const resolvePath = (pathname: string): string => {
  const decoded = decodeURIComponent(pathname);
  let target = decoded;

  if (target.endsWith("/")) {
    target = `${target}index.html`;
  }

  if (!path.extname(target)) {
    target = `${target}.html`;
  }

  return path.join(publicDir, target);
};

const sendResponse = async (
  req: IncomingMessage,
  res: ServerResponse,
  filePath: string,
  statusCode = 200
): Promise<void> => {
  const extension = path.extname(filePath).toLowerCase();
  const mimeType = MIME_TYPES[extension] || "application/octet-stream";
    const cacheControl = extension.match(/\.(?:html)$/)
      ? "no-cache"
      : "public, max-age=86400, immutable";


  const stat = await fs.stat(filePath).catch(() => undefined);
  const data = await fs.readFile(filePath);
  const etag = computeEtag(data);

  // Conditional requests
  const inm = (req.headers["if-none-match"] as string) || "";
  if (inm && inm === etag) {
    res.writeHead(
      304,
      applySecurityHeadersForRequest(req, {
        ETag: etag,
        "Cache-Control": cacheControl,
        ...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
      })
    );
    res.end();
    return;
  }

  let body: any = data;
  const headers: Record<string, string> = {
    "Content-Type": mimeType,
    "Cache-Control": cacheControl,
    ETag: etag,
    ...(stat ? { "Last-Modified": stat.mtime.toUTCString() } : {}),
  };

  // Prefer Brotli over Gzip when supported
  const chosen = pickEncoding(req, mimeType);
  try {
    if (chosen === "br") {
      body = brotliCompressSync(data, {
        params: {
          [zlibConstants.BROTLI_PARAM_QUALITY]: 4, // fast, good ratio for text
          [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
        },
      });
      headers["Content-Encoding"] = "br";
      headers["Vary"] = "Accept-Encoding";
    } else if (chosen === "gzip") {
      body = gzipSync(data);
      headers["Content-Encoding"] = "gzip";
      headers["Vary"] = "Accept-Encoding";
    }
  } catch {
    // Si falla compresión, enviar sin comprimir
  }

  res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));
  res.end(body);
};

const renderTemplate = async (
  req: IncomingMessage,
  res: ServerResponse,
  template: string,
  locals: Record<string, any> = {},
  statusCode = 200
) => {
  const pageFile = path.join(viewsDir, "pages", `${template}.ejs`);
  const layoutFile = path.join(viewsDir, "layouts", "layout.ejs");
  const pageBody = await ejs.renderFile(pageFile, locals, { async: true });
  const defaultTitle = `${
    locals.appName ?? pkg.name ?? "Amayo Bot"
  } | Guía Completa`;
  const html = await ejs.renderFile(
    layoutFile,
    {
      head: null,
      scripts: null,
      ...locals,
      title: locals.title ?? defaultTitle,
      body: pageBody,
    },
    { async: true }
  );
  const htmlBuffer = Buffer.from(html, "utf8");
  const etag = computeEtag(htmlBuffer);

  // Conditional ETag for dynamic page (fresh each deploy change)
  const inm = (req.headers["if-none-match"] as string) || "";
  if (inm && inm === etag) {
    res.writeHead(
      304,
      applySecurityHeadersForRequest(req, {
        ETag: etag,
        "Cache-Control": "no-cache",
      })
    );
    res.end();
    return;
  }

  let respBody: any = htmlBuffer;
  const headers: Record<string, string> = {
    "Content-Type": "text/html; charset=utf-8",
    "Cache-Control": "no-cache",
    ETag: etag,
  };

  const chosenDyn = pickEncoding(req, "text/html; charset=utf-8");
  try {
    if (chosenDyn === "br") {
      respBody = brotliCompressSync(htmlBuffer, {
        params: {
          [zlibConstants.BROTLI_PARAM_QUALITY]: 4,
          [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
        },
      });
      headers["Content-Encoding"] = "br";
      headers["Vary"] = "Accept-Encoding";
    } else if (chosenDyn === "gzip") {
      respBody = gzipSync(htmlBuffer);
      headers["Content-Encoding"] = "gzip";
      headers["Vary"] = "Accept-Encoding";
    }
  } catch {
    // continuar sin comprimir
  }

  res.writeHead(statusCode, applySecurityHeadersForRequest(req, headers));
  res.end(respBody);
};

export const server = createServer(
  async (req: IncomingMessage, res: ServerResponse) => {
    try {
      // 🔒 Forzar HTTPS en producción (Heroku)
      if (process.env.NODE_ENV === "production") {
        const proto = req.headers["x-forwarded-proto"];
        if (proto && proto !== "https") {
          res.writeHead(301, {
            Location: `https://${req.headers.host}${req.url}`,
          });
          return res.end();
        }
      }

      const url = new URL(
        req.url ?? "/",
        `http://${req.headers.host ?? "localhost"}`
      );

      // Basic hardening: respond to robots.txt quickly (optional: disallow all or keep current)
      if (url.pathname === "/robots.txt") {
        const robots = "User-agent: *\nAllow: /\n"; // change to Disallow: / if you want to discourage polite crawlers
        res.writeHead(
          200,
          applySecurityHeadersForRequest(req, {
            "Content-Type": "text/plain; charset=utf-8",
            "Cache-Control": "public, max-age=86400",
          })
        );
        return res.end(robots);
      }

      const clientIp = getClientIp(req);
      if (isSuspiciousPath(url.pathname)) {
        // Hard block known-bad keyword probes
        if (BLOCKED_PATTERNS.some((re) => re.test(url.pathname))) {
          const headers = applySecurityHeadersForRequest(req, {
            "Content-Type": "text/plain; charset=utf-8",
          });
          res.writeHead(403, headers);
          return res.end("Forbidden");
        }
        // Rate limit repetitive suspicious hits per IP
        const rate = hitSuspicious(clientIp);
        if (!rate.allowed) {
          const headers = applySecurityHeadersForRequest(req, {
            "Content-Type": "text/plain; charset=utf-8",
            "Retry-After": String(Math.ceil(rate.resetIn / 1000)),
            "X-RateLimit-Limit": String(RATE_MAX_SUSPICIOUS),
            "X-RateLimit-Remaining": String(rate.remaining),
          });
          res.writeHead(429, headers);
          return res.end("Too Many Requests");
        }
      }

      // Ruta dinámica: renderizar index con EJS
      if (
        url.pathname === "/" ||
        url.pathname === "/index" ||
        url.pathname === "/index.html"
      ) {
        const now = new Date();
        const currentDateHuman = now.toLocaleDateString("es-ES", {
          month: "long",
          year: "numeric",
        });
        const djsVersion = pkg?.dependencies?.["discord.js"] ?? "15.0.0-dev";
        await renderTemplate(req, res, "index", {
          appName: pkg.name ?? "Amayo Bot",
          version: pkg.version ?? "2.0.0",
          djsVersion,
          currentDateHuman,
        });
        return;
      }

      const filePath = resolvePath(url.pathname);

      if (!filePath.startsWith(publicDir)) {
        res.writeHead(403);
        res.end("Forbidden");
        return;
      }

      try {
        await sendResponse(req, res, filePath);
      } catch (error: any) {
        if (error.code === "ENOENT") {
          const notFoundPath = path.join(publicDir, "404.html");
          try {
            await sendResponse(req, res, notFoundPath, 404);
          } catch {
            res.writeHead(
              404,
              applySecurityHeadersForRequest(req, {
                "Content-Type": "text/plain; charset=utf-8",
              })
            );
            res.end("404 - Recurso no encontrado");
          }
        } else if (error.code === "EISDIR") {
          const indexPath = path.join(filePath, "index.html");
          await sendResponse(req, res, indexPath);
        } else {
          console.error("[Server] Error al servir archivo:", error);
          res.writeHead(
            500,
            applySecurityHeadersForRequest(req, {
              "Content-Type": "text/plain; charset=utf-8",
            })
          );
          res.end("500 - Error interno del servidor");
        }
      }
    } catch (error) {
      console.error("[Server] Error inesperado:", error);
      res.writeHead(
        500,
        applySecurityHeadersForRequest(req, {
          "Content-Type": "text/plain; charset=utf-8",
        })
      );
      res.end("500 - Error interno");
    }
  }
);
