import { createServer } from "node:http";
import { dirname, join } from "node:path";
import { loadConfig } from "./config.js";
import { listPublicAccounts, restoreLocalHistorySources } from "./accounts.js";
import { resolveAccountRoute } from "./account-routes.js";
import { createAccountLoginAttempts } from "./account-login-attempts.js";
import { assertAllowedOrigin, assertCorsPreflight, assertEventStreamOrigin, prepareHttpRequest, sendJson, methodNotAllowed, notFound, publicErrorBody, readJson, sendOptions } from "./http.js";
import { listPublicWorkspaces } from "./workspaces.js";
import { resolveWorkspaceRoute } from "./workspace-routes.js";
import { resolveThreadRoute } from "./thread-routes.js";
import { codexHealth } from "./codex-health.js";
import { createCodexRuntimePool } from "./codex-runtime.js";
import { createDeviceAuthStore } from "./device-auth-store.js";
import { createEntitlementStore } from "./entitlement-store.js";
import { resolveEntitlementRoute } from "./entitlement-routes.js";
import { createActionReceiptStore } from "./action-receipt-store.js";
import { expireDeviceAuthentication, resolveDeviceAuthRoute, resolvePublicDeviceAuthRoute, resolveRequestDevice } from "./device-auth-http.js";
import { cleanupPcApplicationDataQuarantines, deletePcApplicationData } from "./application-data-reset.js";
import { resolveApplicationDataRoute } from "./application-data-routes.js";
import { listAccountThreads, listUnifiedThreads, readAccountThread } from "./codex-history.js";
import { projectThreadGroups } from "./thread-groups.js";
import { createProjectPreferencesStore } from "./project-preferences.js";
import { createUsageHistoryStore } from "./usage-history.js";
import { createAccountTokenUsageStore } from "./account-token-usage-store.js";
import { resolveUsageRoute } from "./usage-routes.js";
import { artifactHeaders, resolveArtifactRoute } from "./artifact-routes.js";
import { writeSafeLog } from "./safe-logger.js";
import {
  attachEventStream,
  attachSessionListEventStream,
  createAttachment,
  createSession,
  deleteAttachment,
  deleteQueuedMessage,
  deleteThreadLocalData,
  getAttachment,
  getArtifact,
  getDiff,
  getSession,
  initializeSessions,
  listApprovals,
  listCapabilities,
  listFiles,
  listSessions,
  openThread,
  prepareSessionsForApplicationDataReset,
  publicSession,
  resolveApproval,
  sendMessage,
  stopSession,
  steerQueuedMessage,
  updateThreadSessionTitle
} from "./sessions.js";

const { config, configPath } = await loadConfig();
const applicationLogsDir = process.env.LOCALAPPDATA
  ? join(process.env.LOCALAPPDATA, "CodeRemoteManager", "logs")
  : join(dirname(config.dataDir), "logs");
await cleanupPcApplicationDataQuarantines({ dataDir: config.dataDir, logsDir: applicationLogsDir })
  .catch((error) => writeSafeLog(console, "warn", "application_data_cleanup_deferred", { error }, config));
await restoreLocalHistorySources(config, configPath)
  .catch((error) => writeSafeLog(console, "warn", "account_history_restore_deferred", { error }, config));
const entitlementStore = createEntitlementStore({
  dataDir: config.dataDir,
  publicKeys: config.entitlement?.publicKeys ?? {},
  accountsProvider: () => config.accounts,
  developerPro: process.env.NODE_ENV === "development"
    && process.env.CODE_REMOTE_MANAGER_DEVELOPER_PRO === "1"
});
await entitlementStore.initialize();
const deviceAuthStore = createDeviceAuthStore(config.dataDir);
const deviceAuthenticationRequired = config.deviceAuthentication?.required !== false;
const actionReceiptStore = createActionReceiptStore(config.dataDir);
const tokenUsageStore = createAccountTokenUsageStore(config.dataDir);
const accountLoginAttempts = await createAccountLoginAttempts(config, configPath, {
  tokenUsageStore,
  assertCanAddExecutionAccount: () => entitlementStore.assertCanAddExecutionAccount()
});
const projectPreferences = createProjectPreferencesStore(config.dataDir);
const usageHistory = createUsageHistoryStore(config.dataDir);
const runtimePool = config.engine === "codex" ? createCodexRuntimePool(config) : null;
const historyRequester = runtimePool?.requestHistory;
const readThreadFromPool = historyRequester
  ? (requestConfig, accountId, threadId) => readAccountThread(requestConfig, accountId, threadId, historyRequester)
  : undefined;
await initializeSessions(config, undefined, {
  actionReceiptStore,
  getAccountExecutionAccess: (accountId) => entitlementStore.getAccountExecutionAccess(accountId),
  ...(runtimePool ? {
    startRuntime: runtimePool.startRuntime,
    generateConversationTitle: runtimePool.generateConversationTitle
  } : {})
});

let applicationDataResetting = false;
let managedRestartScheduled = false;
const server = createServer(async (req, res) => {
  try {
    const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
    if (applicationDataResetting && url.pathname !== "/health") {
      const error = new Error("application_data_reset_in_progress");
      error.status = 503;
      error.safeLogCode = "application_data_reset_in_progress";
      throw error;
    }
    const origin = prepareHttpRequest(req, res, config);

    if (req.method === "OPTIONS") {
      assertCorsPreflight(req, config);
      sendOptions(res, origin, config);
      return;
    }

    if (req.method === "GET" && url.pathname === "/health") {
      const authentication = deviceAuthenticationRequired
        ? await deviceAuthStore.status()
        : { paired: false, pairingAvailable: false };
      sendJson(res, 200, { ok: true, authentication: { required: deviceAuthenticationRequired, ...authentication } });
      return;
    }

    if (req.method === "POST" && url.pathname === "/auth/pair") {
      assertAllowedOrigin(req, config);
      const publicAuthRoute = await resolvePublicDeviceAuthRoute(req, url, { store: deviceAuthStore, readJson, res });
      sendJson(res, publicAuthRoute.status, publicAuthRoute.body);
      return;
    }

    const device = await resolveRequestDevice(req, deviceAuthStore, deviceAuthenticationRequired);
    assertAllowedOrigin(req, config);

    const deviceAuthRoute = await resolveDeviceAuthRoute(req, url, { store: deviceAuthStore, device, res });
    if (deviceAuthRoute) {
      sendJson(res, deviceAuthRoute.status, deviceAuthRoute.body);
      return;
    }

    const entitlementRoute = await resolveEntitlementRoute(req, url, { store: entitlementStore, readJson });
    if (entitlementRoute) {
      sendJson(res, entitlementRoute.status, entitlementRoute.body);
      return;
    }

    const applicationDataRoute = await resolveApplicationDataRoute(req, url, {
      readJson,
      managedHost: process.env.CODE_REMOTE_MANAGER_MANAGED_HOST === "1",
      beginReset: () => { applicationDataResetting = true; },
      endReset: () => { applicationDataResetting = false; },
      prepareSessions: prepareSessionsForApplicationDataReset,
      stopRuntime: () => runtimePool?.stop(),
      deleteData: () => deletePcApplicationData({ dataDir: config.dataDir, logsDir: applicationLogsDir }),
      expirePairing: () => expireDeviceAuthentication(res, req),
      requestRestart: requestManagedRestart
    });
    if (applicationDataRoute) {
      sendJson(res, applicationDataRoute.status, applicationDataRoute.body);
      return;
    }

    if (req.method === "GET" && url.pathname === "/accounts") {
      sendJson(res, 200, {
        accounts: await listPublicAccounts(config, undefined, (accountId) => entitlementStore.getAccountExecutionAccess(accountId))
      });
      return;
    }

    const accountRoute = await resolveAccountRoute(req, url, {
      config,
      configPath,
      accountLoginAttempts,
      tokenUsageStore,
      assertCanAddExecutionAccount: () => entitlementStore.assertCanAddExecutionAccount()
    });
    if (accountRoute) {
      sendJson(res, accountRoute.status, accountRoute.body);
      return;
    }

    const workspaceRoute = await resolveWorkspaceRoute(req, url, { config, configPath });
    if (workspaceRoute) {
      sendJson(res, workspaceRoute.status, workspaceRoute.body);
      return;
    }

    const threadRoute = await resolveThreadRoute(req, url, { config, readJson, deleteThreadLocalData, updateThreadSessionTitle, listSessions });
    if (threadRoute) {
      sendJson(res, threadRoute.status, threadRoute.body);
      return;
    }

    const usageRoute = await resolveUsageRoute(req, url, { config, historyStore: usageHistory, tokenUsageStore });
    if (usageRoute) {
      sendJson(res, usageRoute.status, usageRoute.body);
      return;
    }

    const artifactRoute = await resolveArtifactRoute(req, url, { getSession, getArtifact });
    if (artifactRoute) {
      res.writeHead(artifactRoute.status, {
        ...artifactHeaders(artifactRoute.artifact),
        ...(res.corsHeaders ?? {})
      });
      res.end(artifactRoute.artifact.data);
      return;
    }

    if (req.method === "GET" && url.pathname === "/workspaces") {
      sendJson(res, 200, { workspaces: listPublicWorkspaces(config) });
      return;
    }

    if (req.method === "GET" && url.pathname === "/codex/health") {
      sendJson(res, 200, await codexHealth(config));
      return;
    }

    if (req.method === "GET" && url.pathname === "/threads") {
      const search = url.searchParams.get("search") || undefined;
      const archived = url.searchParams.get("archived") === "true";
      const result = await listUnifiedThreads(config, {
        cursor: url.searchParams.get("cursor") || undefined,
        search,
        archived
      }, historyRequester);
      sendJson(res, 200, {
        ...result,
        threads: projectThreadGroups(result.threads, listSessions(), config),
        hiddenProjects: await projectPreferences.hiddenProjects()
      });
      return;
    }

    if (req.method === "GET" && url.pathname === "/thread-projects") {
      const result = await listUnifiedThreads(config, {
        cursor: url.searchParams.get("cursor") || undefined
      }, historyRequester);
      sendJson(res, 200, {
        ...result,
        threads: projectThreadGroups(result.threads, listSessions(), config),
        hiddenProjects: await projectPreferences.hiddenProjects()
      });
      return;
    }

    if (req.method === "POST" && url.pathname === "/thread-projects/hide") {
      const input = await readJson(req);
      sendJson(res, 200, { hiddenProject: await projectPreferences.hide(input?.projectKey) });
      return;
    }

    const accountThreadsMatch = url.pathname.match(/^\/accounts\/([^/]+)\/threads$/);
    if (req.method === "GET" && accountThreadsMatch) {
      const accountId = decodeURIComponent(accountThreadsMatch[1]);
      const result = await listAccountThreads(config, accountId, {
        cursor: url.searchParams.get("cursor") || undefined,
        search: url.searchParams.get("search") || undefined,
        archived: url.searchParams.get("archived") === "true"
      }, historyRequester);
      const linkedSessions = new Map(
        listSessions()
          .filter((session) => session.accountId === accountId && session.codexThreadId)
          .map((session) => [session.codexThreadId, session.id])
      );
      sendJson(res, 200, {
        ...result,
        threads: result.threads.map((thread) => ({
          ...thread,
          linkedSessionId: linkedSessions.get(thread.id)
        }))
      });
      return;
    }

    const accountCapabilitiesMatch = url.pathname.match(/^\/accounts\/([^/]+)\/capabilities$/);
    if (req.method === "GET" && accountCapabilitiesMatch) {
      sendJson(res, 200, await listCapabilities(
        config,
        decodeURIComponent(accountCapabilitiesMatch[1]),
        url.searchParams.get("workspaceId") || undefined
      ));
      return;
    }

    const openAccountThreadMatch = url.pathname.match(
      /^\/accounts\/([^/]+)\/threads\/([^/]+)\/open$/
    );
    if (req.method === "POST" && openAccountThreadMatch) {
      const accountId = decodeURIComponent(openAccountThreadMatch[1]);
      const threadId = decodeURIComponent(openAccountThreadMatch[2]);
      sendJson(res, 200, await openThread(config, accountId, threadId, await readJson(req), readThreadFromPool));
      return;
    }

    if (req.method === "GET" && url.pathname === "/sessions") {
      sendJson(res, 200, { sessions: listSessions() });
      return;
    }

    if (req.method === "GET" && url.pathname === "/sessions/events") {
      assertEventStreamOrigin(req, config);
      attachSessionListEventStream(res);
      return;
    }

    if (req.method === "POST" && url.pathname === "/sessions") {
      const session = await createSession(config, await readJson(req));
      sendJson(res, 201, { session });
      return;
    }

    const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
    if (req.method === "GET" && sessionMatch) {
      sendJson(res, 200, { session: publicSession(getSession(sessionMatch[1])) });
      return;
    }

    const eventsMatch = url.pathname.match(/^\/sessions\/([^/]+)\/events$/);
    if (req.method === "GET" && eventsMatch) {
      assertEventStreamOrigin(req, config);
      attachEventStream(getSession(eventsMatch[1]), res);
      return;
    }

    const messagesMatch = url.pathname.match(/^\/sessions\/([^/]+)\/messages$/);
    if (req.method === "POST" && messagesMatch) {
      const session = getSession(messagesMatch[1]);
      const input = await readJson(req);
      if (typeof input?.clientMessageId !== "string") {
        const error = new Error("client_message_id_required");
        error.status = 400;
        error.safeLogCode = "client_message_id_required";
        throw error;
      }
      const delivery = await sendMessage(session, input);
      sendJson(res, 202, { session: publicSession(session), delivery });
      return;
    }

    const stopMatch = url.pathname.match(/^\/sessions\/([^/]+)\/stop$/);
    if (req.method === "POST" && stopMatch) {
      const session = getSession(stopMatch[1]);
      await stopSession(session);
      sendJson(res, 200, { session: publicSession(session) });
      return;
    }

    const queuedSteerMatch = url.pathname.match(/^\/sessions\/([^/]+)\/queued-messages\/([^/]+)\/steer$/);
    if (req.method === "POST" && queuedSteerMatch) {
      const session = getSession(queuedSteerMatch[1]);
      const delivery = await steerQueuedMessage(session, queuedSteerMatch[2]);
      sendJson(res, 200, { session: publicSession(session), delivery });
      return;
    }

    const queuedMessageMatch = url.pathname.match(/^\/sessions\/([^/]+)\/queued-messages\/([^/]+)$/);
    if (req.method === "DELETE" && queuedMessageMatch) {
      const session = getSession(queuedMessageMatch[1]);
      const delivery = await deleteQueuedMessage(session, queuedMessageMatch[2]);
      sendJson(res, 200, { session: publicSession(session), delivery });
      return;
    }
    const attachmentMatch = url.pathname.match(/^\/sessions\/([^/]+)\/attachments$/);
    if (req.method === "POST" && attachmentMatch) {
      const session = getSession(attachmentMatch[1]);
      const attachment = await createAttachment(session, await readJson(req, 12 * 1024 * 1024));
      sendJson(res, 201, {
        attachment: {
          id: attachment.id,
          kind: attachment.kind,
          name: attachment.name,
          mimeType: attachment.mimeType,
          size: attachment.size,
          createdAt: attachment.createdAt
        }
      });
      return;
    }

    const attachmentReadMatch = url.pathname.match(/^\/sessions\/([^/]+)\/attachments\/([^/]+)$/);
    if (req.method === "GET" && attachmentReadMatch) {
      const attachment = await getAttachment(getSession(attachmentReadMatch[1]), attachmentReadMatch[2]);
      res.writeHead(200, {
        "content-type": attachment.mimeType,
        "x-content-type-options": "nosniff",
        "cache-control": "private, no-store",
        ...(res.corsHeaders ?? {})
      });
      res.end(attachment.data);
      return;
    }
    if (req.method === "DELETE" && attachmentReadMatch) {
      const deletion = await deleteAttachment(getSession(attachmentReadMatch[1]), attachmentReadMatch[2]);
      sendJson(res, 200, { deletion });
      return;
    }

    const approvalMatch = url.pathname.match(/^\/sessions\/([^/]+)\/approvals\/([^/]+)$/);
    if (req.method === "POST" && approvalMatch) {
      const session = getSession(approvalMatch[1]);
      await resolveApproval(session, approvalMatch[2], await readJson(req));
      sendJson(res, 200, { session: publicSession(session) });
      return;
    }

    const approvalsMatch = url.pathname.match(/^\/sessions\/([^/]+)\/approvals$/);
    if (req.method === "GET" && approvalsMatch) {
      sendJson(res, 200, listApprovals(getSession(approvalsMatch[1])));
      return;
    }

    const filesMatch = url.pathname.match(/^\/sessions\/([^/]+)\/files$/);
    if (req.method === "GET" && filesMatch) {
      sendJson(res, 200, await listFiles(getSession(filesMatch[1])));
      return;
    }

    const diffMatch = url.pathname.match(/^\/sessions\/([^/]+)\/diff$/);
    if (req.method === "GET" && diffMatch) {
      sendJson(res, 200, await getDiff(getSession(diffMatch[1]), url.searchParams.get("path")));
      return;
    }

    if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
      methodNotAllowed(res);
      return;
    }

    notFound(res);
  } catch (error) {
    if (error.safeLogCode) writeSafeLog(console, "warn", "request_rejected", { error }, config);
    else writeSafeLog(console, "error", "request_failed", { error }, config);
    sendJson(res, error.status ?? 500, publicErrorBody(error, config));
  }
});

server.listen(config.port, config.bindHost, () => {
  writeSafeLog(console, "info", "backend_listening", { host: config.bindHost, port: config.port }, config);
  void runtimePool?.prewarm();
});

function requestManagedRestart() {
  if (managedRestartScheduled) return;
  managedRestartScheduled = true;
  const restartTimer = setTimeout(() => {
    server.close(() => process.exit(0));
    const forcedExit = setTimeout(() => process.exit(0), 1000);
    forcedExit.unref?.();
  }, 100);
  restartTimer.unref?.();
}
