const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");
const { spawn } = require("node:child_process");
const { URL } = require("node:url");

const listenHost = process.env.CLAUDEQQ_ADAPTER_HOST || "0.0.0.0";
const listenPort = Number.parseInt(process.env.CLAUDEQQ_ADAPTER_PORT || "3290", 10);
const upstreamHost = process.env.CLAUDEQQ_UPSTREAM_HOST || "127.0.0.1";
const upstreamPort = Number.parseInt(process.env.CLAUDEQQ_UPSTREAM_PORT || "9082", 10);
const deployCommit = process.env.UNIDESK_DEPLOY_COMMIT || "";
const deployRequestedCommit = process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT || deployCommit;
const deployRepo = process.env.UNIDESK_DEPLOY_REPO || "";
const serviceId = process.env.UNIDESK_DEPLOY_SERVICE_ID || "claudeqq";
const workspaceDir = process.env.CLAUDEQQ_WORKSPACE_DIR || "/bot_workspace";
const stateDir = process.env.CLAUDEQQ_STATE_DIR || "/app/.state";
const startedAt = new Date().toISOString();
const endpoints = [
  "/health",
  "/logs",
  "/api/push/text",
  "/api/send/text",
  "/api/server/status",
  "/api/napcat/login",
  "/api/events/recent",
  "/api/events/subscriptions",
];

const child = spawn("node", ["dist/index.js", "--max-restarts", "0"], {
  cwd: process.env.CLAUDEQQ_SERVER_CWD || process.cwd(),
  env: {
    ...process.env,
    CLAUDEQQ_HOST: upstreamHost,
    CLAUDEQQ_PORT: String(upstreamPort),
  },
  stdio: "inherit",
});

child.on("exit", (code, signal) => {
  console.error(JSON.stringify({ ts: new Date().toISOString(), service: serviceId, event: "upstream_exited", code, signal }));
  process.exit(code === null ? 1 : code);
});

function json(res, status, body) {
  const text = JSON.stringify(body);
  res.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(text),
  });
  res.end(text);
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
    req.on("end", () => resolve(Buffer.concat(chunks)));
    req.on("error", reject);
  });
}

function asRecord(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function numberOrUndefined(value) {
  const numberValue = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
  return Number.isFinite(numberValue) ? numberValue : undefined;
}

function parseJson(value, fallback) {
  try {
    return JSON.parse(value);
  } catch {
    return fallback;
  }
}

function clampLimit(value) {
  const parsed = Number.parseInt(String(value || "50"), 10);
  if (!Number.isFinite(parsed) || parsed <= 0) return 50;
  return Math.min(parsed, 200);
}

function textFromMessage(message) {
  if (typeof message === "string") return message;
  if (!Array.isArray(message)) return "";
  return message
    .map((segment) => {
      const data = asRecord(asRecord(segment).data);
      if (typeof data.text === "string") return data.text;
      return "";
    })
    .join("");
}

function eventFromRecord(record, fallbackIndex) {
  const item = asRecord(record);
  const raw = asRecord(item.raw_message || item.raw || item);
  const messageId = numberOrUndefined(raw.message_id ?? item.message_id);
  const time = numberOrUndefined(raw.time ?? item.time);
  const userId = raw.user_id ?? item.user_id;
  const groupId = raw.group_id ?? item.group_id;
  const eventId = typeof item.id === "string" && item.id.length > 0
    ? item.id
    : [time || "unknown", messageId || fallbackIndex, userId || "unknown", groupId || "private"].join(":");

  return {
    id: eventId,
    type: typeof item.type === "string" ? item.type : "message",
    source: typeof item.source === "string" ? item.source : "messages-file",
    timestamp: typeof item.timestamp === "string"
      ? item.timestamp
      : time
        ? new Date(time * 1000).toISOString()
        : new Date(0).toISOString(),
    messageType: typeof raw.message_type === "string" ? raw.message_type : (groupId ? "group" : "private"),
    userId: userId === undefined ? undefined : String(userId),
    groupId: groupId === undefined ? undefined : String(groupId),
    messageId,
    text: textFromMessage(raw.message ?? item.message ?? item.text),
    raw,
  };
}

function readRecentEvents(searchParams) {
  const limit = clampLimit(searchParams.get("limit"));
  const since = searchParams.get("since");
  const messagesFile = path.join(workspaceDir, "messages", "messages.jsonl");
  try {
    if (!fs.existsSync(messagesFile)) return [];
    const lines = fs.readFileSync(messagesFile, "utf8").split(/\r?\n/).filter(Boolean);
    const events = lines
      .slice(-Math.max(limit * 2, limit))
      .map((line, index) => eventFromRecord(parseJson(line, {}), index))
      .filter((event) => !since || event.timestamp > since);
    return events.slice(-limit).reverse();
  } catch {
    return [];
  }
}

function readSubscriptions() {
  const subscriptionsFile = path.join(stateDir, "event_subscriptions.json");
  try {
    if (!fs.existsSync(subscriptionsFile)) return [];
    const parsed = parseJson(fs.readFileSync(subscriptionsFile, "utf8"), []);
    return Array.isArray(parsed) ? parsed.map(asRecord) : [];
  } catch {
    return [];
  }
}

function fallbackEventResponse(pathname, searchParams, upstream) {
  if (pathname === "/api/events/recent") {
    const events = readRecentEvents(searchParams);
    return {
      ok: true,
      events,
      count: events.length,
      nextCursor: null,
      source: "adapter-readonly-fallback",
      degraded: true,
      reason: "upstream-event-api-unavailable",
      upstream: { ok: upstream.ok, status: upstream.status },
    };
  }
  const subscriptions = readSubscriptions();
  return {
    ok: true,
    subscriptions,
    count: subscriptions.length,
    source: "adapter-readonly-fallback",
    degraded: true,
    reason: "upstream-event-api-unavailable",
    upstream: { ok: upstream.ok, status: upstream.status },
  };
}

function normalizeEventApiBody(pathname, body, upstream) {
  const record = asRecord(body);
  if (record.ok === true) return record;
  if (pathname === "/api/events/recent" && Array.isArray(record.events)) {
    return { ...record, ok: true, count: record.events.length, source: "upstream", upstream: { ok: upstream.ok, status: upstream.status } };
  }
  if (pathname === "/api/events/subscriptions" && Array.isArray(record.subscriptions)) {
    return { ...record, ok: true, count: record.subscriptions.length, source: "upstream", upstream: { ok: upstream.ok, status: upstream.status } };
  }
  return null;
}

async function eventApiRead(res, url) {
  const upstream = await upstreamFetch(`${url.pathname}${url.search}`);
  if (upstream.ok && typeof upstream.body === "string") {
    const normalized = normalizeEventApiBody(url.pathname, parseJson(upstream.body, null), upstream);
    if (normalized) {
      json(res, 200, normalized);
      return;
    }
  }
  json(res, 200, fallbackEventResponse(url.pathname, url.searchParams, upstream));
}

function normalizePushPayload(raw) {
  const body = JSON.parse(raw.length > 0 ? raw.toString("utf8") : "{}");
  const message = typeof body.message === "string" ? body.message : "";
  if (message.length === 0) throw new Error("message is required");
  if (body.targetType === "group" || body.groupId !== undefined) {
    const groupId = typeof body.groupId === "string" ? body.groupId : String(body.groupId || "");
    if (groupId.length === 0) throw new Error("groupId is required");
    return { groupId, message };
  }
  const userId = typeof body.userId === "string" ? body.userId : String(body.userId || "");
  if (userId.length === 0) throw new Error("userId is required");
  return { userId, message };
}

function upstreamFetch(path, method = "GET") {
  return new Promise((resolve) => {
    const req = http.request({ host: upstreamHost, port: upstreamPort, path, method, timeout: 2000 }, (res) => {
      const chunks = [];
      res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
      res.on("end", () => {
        const status = res.statusCode || 0;
        resolve({ ok: status >= 200 && status < 300, status, body: Buffer.concat(chunks).toString("utf8") });
      });
    });
    req.on("timeout", () => req.destroy(new Error("timeout")));
    req.on("error", (error) => resolve({ ok: false, status: 0, body: String(error?.message || error) }));
    req.end();
  });
}

async function upstreamJson(path) {
  const response = await upstreamFetch(path);
  if (!response.ok || typeof response.body !== "string") return null;
  try {
    return JSON.parse(response.body);
  } catch {
    return null;
  }
}

async function proxy(req, res, path, bodyOverride) {
  const incomingBody = bodyOverride === undefined ? await readBody(req) : Buffer.from(JSON.stringify(bodyOverride));
  const headers = { ...req.headers };
  delete headers.host;
  headers["content-length"] = String(incomingBody.length);
  if (bodyOverride !== undefined) headers["content-type"] = "application/json";

  const options = {
    host: upstreamHost,
    port: upstreamPort,
    method: req.method || "GET",
    path,
    headers,
  };

  const upstream = http.request(options, (upstreamRes) => {
    res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
    upstreamRes.pipe(res);
  });
  upstream.on("error", (error) => {
    json(res, 502, { ok: false, success: false, service: serviceId, error: String(error?.message || error) });
  });
  upstream.end(incomingBody);
}

const server = http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
    if (url.pathname === "/health") {
      const upstreamHealth = await upstreamFetch("/health");
      const status = await upstreamJson("/api/server/status");
      const statusNapcat = status && typeof status === "object" ? status.napcat || {} : {};
      const httpConnected = statusNapcat.httpConnected === true;
      const wsConnected = statusNapcat.wsConnected === true;
      const connected = statusNapcat.connected === true || (httpConnected && wsConnected);
      json(res, upstreamHealth.ok ? 200 : 503, {
        ok: upstreamHealth.ok,
        service: serviceId,
        pureBackend: true,
        endpoints,
        napcat: {
          containerized: true,
          connected,
          httpConnected,
          wsConnected,
          loginState: connected ? "logged_in" : "unknown",
          httpHost: statusNapcat.httpHost || upstreamHost,
          httpPort: statusNapcat.httpPort || 3000,
          wsHost: statusNapcat.wsHost || upstreamHost,
          wsPort: statusNapcat.wsPort || 3001,
        },
        adapter: "unidesk-claudeqq-adapter",
        eventApi: {
          contract: "claudeqq-event-api-v1",
          readonlyFallback: true,
          validationPaths: ["/api/events/recent", "/api/events/subscriptions"],
          mutationPaths: ["POST /api/events/subscriptions", "DELETE /api/events/subscriptions/{id}"],
        },
        startedAt,
        upstream: { url: `http://${upstreamHost}:${upstreamPort}`, ok: upstreamHealth.ok, status: upstreamHealth.status },
        deploy: {
          repo: deployRepo,
          commit: deployCommit,
          requestedCommit: deployRequestedCommit,
        },
      });
      return;
    }
    if (url.pathname === "/api/push/text" && req.method === "POST") {
      const body = normalizePushPayload(await readBody(req));
      await proxy({ ...req, method: "POST" }, res, "/api/send/text", body);
      return;
    }
    if ((url.pathname === "/api/events/recent" || url.pathname === "/api/events/subscriptions") && req.method === "GET") {
      await eventApiRead(res, url);
      return;
    }
    await proxy(req, res, `${url.pathname}${url.search}`, undefined);
  } catch (error) {
    json(res, 400, { ok: false, success: false, service: serviceId, error: String(error?.message || error) });
  }
});

server.listen(listenPort, listenHost, () => {
  console.log(JSON.stringify({ ts: new Date().toISOString(), service: serviceId, event: "adapter_listening", listenHost, listenPort, upstreamHost, upstreamPort }));
});
