/**
 * Development server with same-origin API proxy.
 * Equivalent to Angular's proxy.conf.json + ng serve.
 *
 * Port 8080 (browser-facing):
 *   /api/*      → curl → https://flights.test.aeroflot.ru (bypasses WAF via curl TLS)
 *   /flights/*  → curl → https://flights.test.aeroflot.ru
 *   /map/*      → curl → https://flights.test.aeroflot.ru (binary JPEG tiles)
 *   /tracker/*  → https://platform.test.aeroflot.ru (SignalR, same-origin)
 *   /*          → localhost:8081 (Modern.js SSR + HMR)
 */
import express from "express";
import { createProxyMiddleware, responseInterceptor } from "http-proxy-middleware";
import { HttpsProxyAgent } from "https-proxy-agent";
import { execFile, spawn } from "node:child_process";
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";

const PUBLIC_PORT = 8080;
const MODERNJS_PORT = 8081;
const API_TARGET = process.env.API_TARGET || "https://flights.test.aeroflot.ru";
const TRACKER_TARGET = process.env.TRACKER_TARGET || "https://platform.test.aeroflot.ru";
const SYSTEM_PROXY = process.env.https_proxy || process.env.HTTPS_PROXY || "";
const DEBUG_PROXY_BODY = process.env.DEBUG_PROXY_BODY === "1";

// Shared cookie jar so the Ngenix WAF cookie challenge (`ngenix_valid` +
// 307-to-self) only runs once per dev-server lifetime, not per request.
// We keep one jar per transport (direct vs. HTTPS_PROXY) because the WAF
// anchors the cookie to the upstream edge node that minted it; mixing
// jars across transports re-triggers the challenge on every request.
const COOKIE_JAR_DIRECT = resolve(tmpdir(), `aeroflot-dev-cookies-direct-${process.pid}.txt`);
const COOKIE_JAR_PROXY = resolve(tmpdir(), `aeroflot-dev-cookies-proxy-${process.pid}.txt`);

// --- Start Modern.js on internal port ---
console.log(`Starting Modern.js on :${MODERNJS_PORT}...`);

// Resolve the modern binary directly to avoid DEP0190 (shell: true with spawn)
const modernBin = resolve("node_modules", ".bin", "modern");
const modernProcess = existsSync(modernBin)
  ? spawn(modernBin, ["dev"], {
      stdio: "inherit",
      env: { ...process.env, PORT: String(MODERNJS_PORT) },
    })
  : spawn(process.execPath, [resolve("node_modules", "@modern-js/app-tools", "bin", "modern.js"), "dev"], {
      stdio: "inherit",
      env: { ...process.env, PORT: String(MODERNJS_PORT) },
    });
modernProcess.on("error", (err) => {
  console.error("Modern.js failed:", err);
  process.exit(1);
});

await new Promise((r) => setTimeout(r, 18000));

const app = express();

// --- Tile proxy via curl (binary JPEG, pipe stdout straight to response) ---
const TILE_CONTENT_TYPES = {
  ".jpeg": "image/jpeg",
  ".jpg":  "image/jpeg",
  ".png":  "image/png",
  ".webp": "image/webp",
};
app.use("/map", (req, res) => {
  const targetUrl = `${API_TARGET}${req.originalUrl}`;
  const ext = req.path.substring(req.path.lastIndexOf("."));
  const contentType = TILE_CONTENT_TYPES[ext] ?? "application/octet-stream";

  res.status(200);
  res.setHeader("Content-Type", contentType);
  res.setHeader("Cache-Control", "public, max-age=86400");

  const commonHeaders = [
    "-H", `Accept: image/webp,image/jpeg,image/*,*/*`,
    "-H", `User-Agent: ${req.headers["user-agent"] || "Mozilla/5.0"}`,
    "-H", `Referer: ${API_TARGET}/`,
  ];
  // Tile transport is binary — fall back to the proxy path only if the
  // direct attempt emits no data (indicating WAF deny / throttle).
  const tryTile = (noproxy) => {
    const args = [
      "-sS",
      "-f",                                      // fail on HTTP 4xx/5xx
      "-L",                                      // follow Ngenix 307-to-self cookie challenge
      ...(noproxy ? ["--noproxy", "*"] : []),
      "-c", noproxy ? COOKIE_JAR_DIRECT : COOKIE_JAR_PROXY,
      "-b", noproxy ? COOKIE_JAR_DIRECT : COOKIE_JAR_PROXY,
      ...commonHeaders,
      targetUrl,
    ];
    return spawn("/usr/bin/curl", args, { stdio: ["ignore", "pipe", "pipe"] });
  };
  const child = tryTile(true);
  let gotData = false;
  child.stdout.on("data", (chunk) => { gotData = true; res.write(chunk); });
  child.on("exit", (code) => {
    if (code === 0) { res.end(); return; }
    if (gotData) { res.end(); return; }
    // Direct failed with no data — retry through the system HTTPS_PROXY.
    const retry = tryTile(false);
    retry.stdout.pipe(res);
    retry.on("exit", () => res.end());
  });
});

// --- API proxy via curl (bypasses WAF TLS fingerprinting) ---
// Two transports: (1) direct with `--noproxy '*'` — fast path when the
// upstream is publicly reachable; (2) through the system HTTPS_PROXY
// (e.g. a local gost VPN tunnel) — required when the direct IP hits a
// WAF throttle (403 deny page). Try direct first; on 4xx/5xx or
// network error, retry through the proxy. Each transport keeps its own
// cookie jar because the WAF cookie is bound to the edge node that
// minted it.
app.use(["/api", "/flights"], (req, res) => {
  console.log(`API proxy called for: ${req.originalUrl}`);
  const targetUrl = `${API_TARGET}${req.originalUrl}`;
  console.log(`targetUrl: ${targetUrl}`);

  const buildArgs = (noproxy) => [
    "-s",
    "-L",
    ...(noproxy ? ["--noproxy", "*"] : []),
    "-c", noproxy ? COOKIE_JAR_DIRECT : COOKIE_JAR_PROXY,
    "-b", noproxy ? COOKIE_JAR_DIRECT : COOKIE_JAR_PROXY,
    "-H", `Accept: ${req.headers.accept || "application/json"}`,
    "-H", `User-Agent: ${req.headers["user-agent"] || "Mozilla/5.0"}`,
    "-H", `Accept-Language: ${req.headers["accept-language"] || "ru"}`,
    "-w", "\n%{http_code}",
    targetUrl,
  ];

  if (req.method === "POST") {
    let body = "";
    req.on("data", (chunk) => { body += chunk; });
    req.on("end", () => {
      const bodyArgs = body ? ["-X", "POST", "-d", body, "-H", "Content-Type: application/json"] : ["-X", "POST"];
      execCurlWithFallback(buildArgs, bodyArgs, res);
    });
  } else {
    execCurlWithFallback(buildArgs, [], res);
  }
});

function normalizeTrackerCookie(cookie) {
  if (!cookie.startsWith("signal-id=")) return cookie;

  const parts = cookie
    .split(";")
    .map((part) => part.trim())
    .filter(
      (part) =>
        part.length > 0 &&
        !part.toLowerCase().startsWith("domain=") &&
        !part.toLowerCase().startsWith("path=") &&
        !part.toLowerCase().startsWith("samesite="),
    );

  return [...parts, "Path=/tracker/hub", "SameSite=Lax"].join("; ");
}

function applyTrackerCookieHeaders(proxyRes, res) {
  const setCookie = proxyRes.headers["set-cookie"];
  if (Array.isArray(setCookie)) {
    res.setHeader("set-cookie", setCookie.map(normalizeTrackerCookie));
  } else if (typeof setCookie === "string") {
    res.setHeader("set-cookie", normalizeTrackerCookie(setCookie));
  }
}

function isTrackerLongPollTimeout(proxyRes, req) {
  const requestUrl = req.originalUrl ?? req.url ?? "";
  return (
    req.method === "GET" &&
    (requestUrl.startsWith("/tracker/hub?id=") ||
      requestUrl.startsWith("/hub?id=")) &&
    proxyRes.statusCode === 504
  );
}

// --- SignalR TrackerHub proxy ---
// Browser-direct localhost → platform.test.aeroflot.ru fails CORS. Keep the
// hub same-origin in development and let proxy-helper / gost route the
// upstream request through the TIM tunnel when HTTPS_PROXY is set.
const trackerProxy = createProxyMiddleware({
  target: TRACKER_TARGET,
  changeOrigin: true,
  secure: false,
  ws: true,
  logLevel: "warn",
  pathRewrite: (path) => `/tracker${path}`,
  selfHandleResponse: true,
  on: {
    proxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => {
      applyTrackerCookieHeaders(proxyRes, res);

      if (isTrackerLongPollTimeout(proxyRes, req)) {
        console.warn(`Tracker long poll timed out upstream, returning empty 200 for ${req.originalUrl ?? req.url}`);
        res.statusCode = 200;
        res.statusMessage = "OK";
        res.setHeader("content-type", "text/plain");
        return "";
      }

      return buffer;
    }),
  },
  ...(SYSTEM_PROXY ? { agent: new HttpsProxyAgent(SYSTEM_PROXY) } : {}),
});
app.use("/tracker", trackerProxy);

function execCurlWithFallback(buildArgs, extraArgs, res) {
  runCurl([...extraArgs, ...buildArgs(true)], (direct) => {
    if (isSuccessfulUpstream(direct)) {
      respondWithCurlResult(direct, res);
      return;
    }
    // Direct hit a WAF deny / throttle / network failure — retry through
    // the system HTTPS_PROXY (gost VPN tunnel on this host).
    runCurl([...extraArgs, ...buildArgs(false)], (proxy) => {
      respondWithCurlResult(isSuccessfulUpstream(proxy) ? proxy : direct, res);
    });
  });
}

function runCurl(args, cb) {
  execFile("/usr/bin/curl", args, { maxBuffer: 10 * 1024 * 1024, timeout: 30000 }, (err, stdout, stderr) => {
    if (err) {
      console.warn(`curl failed: ${err.message}`);
    }
    if (stderr) {
      console.warn(`curl stderr: ${stderr.substring(0, 300)}`);
    }
    cb({ err, stdout: stdout ?? "" });
  });
}

function isSuccessfulUpstream({ err, stdout }) {
  if (err) return false;
  const lastNewline = stdout.lastIndexOf("\n");
  const statusStr = lastNewline >= 0 ? stdout.substring(lastNewline + 1).trim() : "";
  const status = parseInt(statusStr) || 0;
  if (status < 200 || status >= 400) return false;

  const body = lastNewline >= 0 ? stdout.substring(0, lastNewline) : stdout;
  const trimmed = body.trimStart();
  // Some upstream edges return the Angular shell as HTTP 200 for API
  // paths on the direct transport. Treat that as a failed API response so
  // the fallback transport can fetch the JSON payload.
  if (trimmed.startsWith("<!DOCTYPE html") || trimmed.startsWith("<html")) {
    return false;
  }

  return true;
}

function respondWithCurlResult({ err, stdout }, res) {
  if (err) {
    res.status(502).json({ error: err.message });
    return;
  }

  // stdout = body + "\n" + statusCode (from -w "\n%{http_code}"). When
  // upstream returns an empty body the stdout starts with "\n", so a
  // `> 0` check mis-treats the status as body and falls back to 200 —
  // silently hiding real 4xx/5xx responses. Use `>= 0` and split
  // unconditionally when the newline is present.
  const lastNewline = stdout.lastIndexOf("\n");
  const body = lastNewline >= 0 ? stdout.substring(0, lastNewline) : stdout;
  const statusStr = lastNewline >= 0 ? stdout.substring(lastNewline + 1).trim() : "200";
  const status = parseInt(statusStr) || 200;

  const isJson = body.trimStart().startsWith("{") || body.trimStart().startsWith("[");

  console.log(
    `proxy response: status=${status}, content=${isJson ? "json" : "html"}, bodyLength=${body.length}, ${summarizeBody(body)}`,
  );
  if (DEBUG_PROXY_BODY) {
    console.log(`body preview: ${body.substring(0, 500)}`);
  }

  res.status(status);
  res.set("Content-Type", isJson ? "application/json" : "text/html");
  res.set("Access-Control-Allow-Origin", "*");
  res.send(body);
}

function summarizeBody(body) {
  const trimmed = body.trimStart();
  if (!trimmed) return "body=empty";

  try {
    const parsed = JSON.parse(trimmed);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      if (typeof parsed.days === "string") {
        const available = [...parsed.days].filter((d) => d === "1").length;
        return `daysLength=${parsed.days.length}, availableDays=${available}`;
      }
      if (parsed.data && Array.isArray(parsed.data.routes)) {
        return `routes=${parsed.data.routes.length}`;
      }
      return `jsonKeys=${Object.keys(parsed).slice(0, 5).join(",")}`;
    }
    if (Array.isArray(parsed)) {
      return `jsonArrayLength=${parsed.length}`;
    }
  } catch {
    // fall through to non-JSON summary
  }

  return `bodyPreview=${trimmed.substring(0, 80).replace(/\s+/g, " ")}`;
}

// --- Everything else → Modern.js ---
const modernProxy = createProxyMiddleware({
  target: `http://localhost:${MODERNJS_PORT}`,
  changeOrigin: false,
  ws: true,
  logLevel: "silent",
});
app.use(modernProxy);

const server = app.listen(PUBLIC_PORT, () => {
  console.log(`\n  ✓ Dev server: http://localhost:${PUBLIC_PORT}`);
  console.log(`    /api/*     → curl → ${API_TARGET}`);
  console.log(`    /tracker/* → proxy → ${TRACKER_TARGET}`);
  console.log(`    /*         → Modern.js :${MODERNJS_PORT}\n`);
});

// Forward WebSocket upgrades to Modern.js HMR server explicitly,
// preventing reconnection spam from http-proxy-middleware's built-in ws handling.
server.on("upgrade", (req, socket, head) => {
  if (req.url?.startsWith("/tracker")) {
    trackerProxy.upgrade(req, socket, head);
    return;
  }
  modernProxy.upgrade(req, socket, head);
});

process.on("SIGINT", () => { modernProcess.kill(); process.exit(); });
process.on("SIGTERM", () => { modernProcess.kill(); process.exit(); });
