import { createServer, request as httpRequest } from "node:http";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { gzip } from "node:zlib";
import { promisify } from "node:util";

import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs";
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";

const readOnlyRpcMethods = new Set([
  "system.health",
  "cloud.adapter.describe",
  "audit.event.query",
  "evidence.record.query"
]);
const gzipAsync = promisify(gzip);
const GZIP_MIN_BYTES = 1024;
const DEFAULT_AUTH_USERNAME = "admin";
const DEFAULT_AUTH_PASSWORD = "hwlab2026";
const DEFAULT_WORKBENCH_PROJECT_ID = "prj_hwpod_workbench";
const CLIENT_DISCONNECT_ERROR_CODES = new Set([
  "ECONNRESET",
  "EPIPE",
  "ERR_STREAM_PREMATURE_CLOSE"
]);
const CLIENT_DISCONNECT_ERROR_PATTERN = /\b(?:socket hang up|client disconnected|premature close)\b/iu;
const CLOUD_WEB_ERROR_HANDLER_INSTALLED = Symbol.for("hwlab.cloud-web.error-handler-installed");

export async function serveCloudWeb(options) {
  installCloudWebProcessErrorHandlers({ serviceId: options.serviceId });
  const server = createCloudWebServer(options);
  server.listen(options.port, "0.0.0.0", () => {
    process.stdout.write(JSON.stringify({
      serviceId: options.serviceId,
      environment: process.env.HWLAB_ENVIRONMENT || "dev",
      status: "listening",
      port: options.port
    }) + "\n");
  });
}

export function createCloudWebServer({
  serviceId,
  healthPayload,
  sendJson,
  roots = ["/app/web/hwlab-cloud-web/dist", "/app/web/hwlab-cloud-web"],
  cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || "",
  cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 1260000, {
    min: 1000,
    max: 2400000
  })
}) {
  return createServer(async (request, response) => {
    attachRequestErrorHandlers({ request, response, serviceId });
    const url = new URL(request.url || "/", "http://hwlab-cloud-web.local");
    try {
      if (url.pathname === "/health/live" || url.pathname === "/health") {
        sendJson(response, 200, healthPayload());
        return;
      }
      if (await handleCloudWebAuth({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson })) {
        return;
      }

      const proxyPolicy = cloudWebProxyRoutePolicy(request.method, url.pathname);
      if (proxyPolicy.proxy) {
        await proxyCloudApi({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson });
        return;
      }

      const routePath = url.pathname.replace(/\/+$/u, "") || "/";
      const relativePath = routePath === "/" || routePath === "/gate" || routePath === "/diagnostics/gate" || routePath === "/help" || routePath === "/skills" || routePath === "/access"
        ? "index.html"
        : url.pathname.slice(1);
      for (const root of roots) {
        const candidate = path.resolve(root, relativePath);
        if (!candidate.startsWith(root)) continue;
        try {
          const info = await stat(candidate);
          if (!info.isFile()) continue;
          const body = await readFile(candidate);
          const type = contentType(candidate);
          const encoded = await encodeStaticBody(request, body, type);
          response.writeHead(200, {
            "content-type": type,
            ...encoded.headers,
            "content-length": encoded.body.length
          });
          response.end(encoded.body);
          return;
        } catch (error) {
          if (isClientDisconnectError(error)) {
            logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error });
            return;
          }
          // Try the next root for ordinary filesystem misses.
        }
      }

      sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname });
    } catch (error) {
      if (isClientDisconnectError(error)) {
        logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error });
        return;
      }
      if (!response.headersSent && !response.writableEnded) {
        sendJson(response, 500, {
          error: "internal_error",
          serviceId,
          message: error instanceof Error ? error.message : String(error)
        });
        return;
      }
      response.destroy(error);
    }
  });
}

export function installCloudWebProcessErrorHandlers({ serviceId } = {}) {
  if (process[CLOUD_WEB_ERROR_HANDLER_INSTALLED]) return;
  process[CLOUD_WEB_ERROR_HANDLER_INSTALLED] = true;
  process.on("uncaughtException", (error) => {
    if (isClientDisconnectError(error)) {
      logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, error, source: "uncaughtException" });
      return;
    }
    logCloudWebRuntimeEvent("cloud-web-runtime-fatal", { serviceId, error, source: "uncaughtException" });
    process.exit(1);
  });
  process.on("unhandledRejection", (reason) => {
    const error = reason instanceof Error ? reason : new Error(String(reason));
    if (isClientDisconnectError(error)) {
      logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, error, source: "unhandledRejection" });
      return;
    }
    logCloudWebRuntimeEvent("cloud-web-runtime-fatal", { serviceId, error, source: "unhandledRejection" });
    process.exit(1);
  });
}

function attachRequestErrorHandlers({ request, response, serviceId }) {
  const handle = (error) => {
    if (isClientDisconnectError(error)) {
      logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error });
      return;
    }
    throw error;
  };
  request.on("error", handle);
  response.on("error", handle);
}

export function isClientDisconnectError(error) {
  const candidate = error instanceof Error ? error : new Error(String(error));
  const code = typeof candidate.code === "string" ? candidate.code : "";
  return CLIENT_DISCONNECT_ERROR_CODES.has(code) || CLIENT_DISCONNECT_ERROR_PATTERN.test(candidate.message || "");
}

function logCloudWebRuntimeEvent(event, { serviceId, request = null, error = null, source = null } = {}) {
  const payload = {
    event,
    serviceId: serviceId || "hwlab-cloud-web",
    source,
    method: request?.method || null,
    path: request?.url || null,
    error: error
      ? {
          name: error.name || "Error",
          code: typeof error.code === "string" ? error.code : null,
          message: error.message || String(error)
        }
      : null,
    observedAt: new Date().toISOString()
  };
  process.stderr.write(JSON.stringify(payload) + "\n");
}

export async function handleCloudWebAuth(options) {
  const { request, response, url } = options;
  if (url.pathname === "/auth/workspace-bootstrap" && request.method === "GET") {
    await handleWorkspaceBootstrap(options);
    return true;
  }
  if (url.pathname === "/auth/bootstrap" && request.method === "GET") {
    await handleAuthBootstrap(options);
    return true;
  }
  if (url.pathname === "/auth/session" && request.method === "GET") {
    await proxyCloudApi(options);
    return true;
  }
  if (url.pathname === "/auth/login" && request.method === "POST") {
    await proxyCloudApi(options);
    return true;
  }
  if (url.pathname === "/auth/logout" && request.method === "POST") {
    await proxyCloudApi(options);
    return true;
  }
  if (!url.pathname.startsWith("/auth/")) return false;
  await proxyCloudApi(options);
  return true;
}

async function handleAuthBootstrap(options) {
  const { request, response, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson } = options;
  if (!cloudApiBaseUrl) {
    sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: "/auth/bootstrap" });
    return;
  }

  const auth = await resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs });
  sendJsonWithForwardedCookies(response, auth.statusCode || 502, auth.body ?? { authenticated: false, error: "auth_bootstrap_failed" }, auth.headers);
}

async function handleWorkspaceBootstrap(options) {
  const { request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson } = options;
  if (!cloudApiBaseUrl) {
    sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: "/auth/workspace-bootstrap" });
    return;
  }

  const auth = await resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs });
  if (auth.body?.authenticated !== true) {
    sendJsonWithForwardedCookies(response, auth.statusCode || 200, auth.body ?? { authenticated: false }, auth.headers);
    return;
  }

  const projectId = authConfigValueFromSearch(url.searchParams.get("projectId"), DEFAULT_WORKBENCH_PROJECT_ID);
  const workspace = await requestCloudApiJson({
    request,
    method: "GET",
    pathname: `/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`,
    headers: auth.cookieHeader ? { cookie: auth.cookieHeader } : {},
    cloudApiBaseUrl,
    cloudApiProxyTimeoutMs
  });
  const body = {
    ...(auth.body ?? { authenticated: true }),
    projectId,
    workspace: workspace.ok ? workspace.body?.workspace ?? null : null,
    workspaceStatus: workspace.statusCode || null,
    workspaceError: workspace.ok ? null : workspace.body?.error ?? workspace.body?.message ?? null
  };
  sendJsonWithForwardedCookies(response, auth.statusCode || 200, body, auth.headers);
}

async function resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) {
  const config = webAuthConfig();
  if (config.mode !== "auto" || hasCookieHeader(request.headers.cookie)) {
    const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs });
    if (session.ok && session.body?.authenticated === true) return withCookieHeader(request, session);
    if (config.mode !== "auto") return withCookieHeader(request, session);
  }

  const login = await requestCloudApiJson({
    request,
    method: "POST",
    pathname: "/auth/login",
    body: JSON.stringify({ username: config.username, password: config.password }),
    cloudApiBaseUrl,
    cloudApiProxyTimeoutMs
  });
  return withCookieHeader(request, login);
}

function webAuthConfig() {
  return {
    mode: authConfigValue("HWLAB_CLOUD_WEB_AUTH_MODE", "auto"),
    username: authConfigValue("HWLAB_CLOUD_WEB_AUTH_USERNAME", DEFAULT_AUTH_USERNAME),
    password: authConfigValue("HWLAB_CLOUD_WEB_AUTH_PASSWORD", DEFAULT_AUTH_PASSWORD)
  };
}

function authConfigValue(name, fallback) {
  const value = process.env[name];
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
}

function authConfigValueFromSearch(value, fallback) {
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
}

function requestCloudApiJson({ request, method, pathname, body = "", headers = {}, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) {
  return new Promise((resolve) => {
    let settled = false;
    const target = new URL(pathname, cloudApiBaseUrl);
    const proxyRequest = { method, headers: { ...upstreamRequestHeaders({ headers: request.headers }, body), ...headers } };
    const upstream = httpRequest(target, { method, headers: proxyRequest.headers }, (upstreamResponse) => {
      const chunks = [];
      upstreamResponse.on("data", (chunk) => chunks.push(chunk));
      upstreamResponse.on("end", () => {
        if (settled) return;
        settled = true;
        const rawBody = Buffer.concat(chunks).toString("utf8");
        let parsed = null;
        try { parsed = rawBody ? JSON.parse(rawBody) : null; } catch { parsed = { error: "invalid_json", rawBody: rawBody.slice(0, 200) }; }
        resolve({ ok: (upstreamResponse.statusCode || 0) >= 200 && (upstreamResponse.statusCode || 0) < 300, statusCode: upstreamResponse.statusCode || 502, body: parsed, headers: upstreamResponse.headers });
      });
      upstreamResponse.on("error", (error) => {
        if (settled) return;
        settled = true;
        resolve({ ok: false, statusCode: 502, body: { authenticated: false, error: error.message }, headers: {} });
      });
    });
    const timeout = setTimeout(() => {
      if (settled) return;
      settled = true;
      upstream.destroy();
      resolve({ ok: false, statusCode: 504, body: { authenticated: false, error: "auth_bootstrap_timeout" }, headers: {} });
    }, cloudApiProxyTimeoutMs);
    upstream.on("error", (error) => {
      if (settled) return;
      settled = true;
      clearTimeout(timeout);
      resolve({ ok: false, statusCode: 502, body: { authenticated: false, error: error.message }, headers: {} });
    });
    upstream.on("close", () => clearTimeout(timeout));
    if (body) upstream.write(body);
    upstream.end();
  });
}

function withCookieHeader(request, result) {
  return {
    ...result,
    cookieHeader: cookieHeaderForUpstream(request.headers.cookie, result.headers?.["set-cookie"])
  };
}

function cookieHeaderForUpstream(existingCookie, setCookie) {
  const pairs = new Map();
  for (const pair of cookiePairsFromHeader(existingCookie)) pairs.set(pair.name, pair.value);
  for (const pair of cookiePairsFromSetCookie(setCookie)) pairs.set(pair.name, pair.value);
  return Array.from(pairs.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
}

function cookiePairsFromHeader(header) {
  if (!header) return [];
  return String(header).split(";").map((part) => cookiePair(part)).filter(Boolean);
}

function hasCookieHeader(header) {
  return cookiePairsFromHeader(header).length > 0;
}

function cookiePairsFromSetCookie(setCookie) {
  const values = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : [];
  return values.map((value) => cookiePair(String(value).split(";")[0])).filter(Boolean);
}

function cookiePair(value) {
  const index = String(value).indexOf("=");
  if (index <= 0) return null;
  const name = String(value).slice(0, index).trim();
  const cookieValue = String(value).slice(index + 1).trim();
  if (!name || !cookieValue) return null;
  return { name, value: cookieValue };
}

function sendJsonWithForwardedCookies(response, statusCode, body, headers = {}) {
  const payload = JSON.stringify(body);
  const setCookie = headers["set-cookie"];
  const responseHeaders = {
    "content-type": "application/json",
    "content-length": Buffer.byteLength(payload)
  };
  if (setCookie) responseHeaders["set-cookie"] = setCookie;
  response.writeHead(statusCode, responseHeaders);
  response.end(payload);
}

async function readRequestBody(request) {
  let body = "";
  for await (const chunk of request) {
    body += chunk;
  }
  return body;
}

function parseTimeout(value, fallback, { min, max }) {
  const parsed = Number.parseInt(value || "", 10);
  if (!Number.isInteger(parsed)) return fallback;
  return Math.min(Math.max(parsed, min), max);
}

function requestUpstream({ target, request, response, body, url, cloudApiProxyTimeoutMs }) {
  return proxyCloudApiRequest({
    target,
    request,
    response,
    body,
    timeoutMs: cloudApiProxyTimeoutMs,
    forceStream: isCloudWebSseRoute(url.pathname)
  });
}

export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson }) {
  if (!cloudApiBaseUrl) {
    sendJson(response, 503, {
      error: "cloud_api_base_url_missing",
      serviceId,
      path: url.pathname
    });
    return;
  }

  let body = "";
  if (request.method !== "GET") {
    body = await readRequestBody(request);
  }

  if (url.pathname === "/json-rpc") {
    try {
      const envelope = body ? JSON.parse(body) : {};
      if (!readOnlyRpcMethods.has(envelope.method)) {
        sendJson(response, 403, {
          error: "readonly_rpc_required",
          serviceId,
          method: envelope.method || null
        });
        return;
      }
    } catch (error) {
      sendJson(response, 400, {
        error: "invalid_json",
        serviceId,
        reason: error.message
      });
      return;
    }
  }

  const target = new URL(url.pathname + url.search, cloudApiBaseUrl);
  try {
    await requestUpstream({ target, request, response, body: request.method === "GET" ? "" : body, url, cloudApiProxyTimeoutMs });
  } catch (error) {
    if (response.headersSent || response.writableEnded) {
      if (!response.writableEnded) response.destroy(error);
      return;
    }
    const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message);
    const code = timedOut ? "cloud_api_proxy_timeout" : "cloud_api_proxy_failed";
    const category = timedOut ? "timeout" : "proxy";
    const traceId = request.headers["x-trace-id"] || null;
    const userMessage = timedOut
      ? "Code Agent 代理等待 cloud-api 超过 " + cloudApiProxyTimeoutMs + "ms；输入已保留，可稍后重试。"
      : "Code Agent 代理暂时无法连接 cloud-api；输入已保留，可稍后重试。";
    sendJson(response, 502, {
      status: "failed",
      error: {
        code,
        layer: "proxy",
        category,
        retryable: true,
        userMessage,
        message: userMessage,
        timeoutMs: timedOut ? cloudApiProxyTimeoutMs : null,
        traceId,
        route: url.pathname,
        blocker: {
          code,
          layer: "proxy",
          category,
          retryable: true,
          summary: userMessage,
          userMessage,
          traceId,
          route: url.pathname
        }
      },
      serviceId,
      target: target.href.replace(/\/\/[^/@]*@/u, "//***@"),
      traceId,
      reason: userMessage
    });
  }
}

function contentType(filePath) {
  if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
  if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
  if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
  if (filePath.endsWith(".svg")) return "image/svg+xml; charset=utf-8";
  if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
  if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
  return "application/octet-stream";
}

async function encodeStaticBody(request, body, type) {
  if (!isGzipCandidate(type, body.length) || !acceptsEncoding(request.headers["accept-encoding"], "gzip")) {
    return { body, headers: isCompressibleType(type) ? { "vary": "Accept-Encoding" } : {} };
  }
  const compressed = await gzipAsync(body, { level: 6 });
  return {
    body: compressed,
    headers: {
      "content-encoding": "gzip",
      "vary": "Accept-Encoding"
    }
  };
}

function isGzipCandidate(type, sizeBytes) {
  return sizeBytes >= GZIP_MIN_BYTES && isCompressibleType(type);
}

function isCompressibleType(type) {
  return /^(?:text\/|application\/json\b)/u.test(String(type || ""));
}

function acceptsEncoding(header, encoding) {
  const value = Array.isArray(header) ? header.join(",") : String(header || "");
  for (const segment of value.split(",")) {
    const [name, ...params] = segment.trim().split(";");
    const token = name.trim().toLowerCase();
    if (token !== encoding && token !== "*") continue;
    const q = params.map((param) => param.trim().toLowerCase()).find((param) => param.startsWith("q="));
    if (q && Number.parseFloat(q.slice(2)) <= 0) continue;
    return true;
  }
  return false;
}
