import { createApp, type Config } from "./app.js";

const config: Config = {
  host: process.env.TRAPOS_SERVER_HOST ?? "0.0.0.0",
  port: readPort(process.env.TRAPOS_SERVER_PORT, 4444),
  serverPassword: requireSecret("TRAPOS_SERVER_PASSWORD", process.env.TRAPOS_SERVER_PASSWORD),
  requestTimeoutMs: readPositiveInt(process.env.TRAPOS_SERVER_REQUEST_TIMEOUT_MS, 600_000),
  opencodeUrl: process.env.OPENCODE_URL ?? "http://127.0.0.1:4242",
  opencodeUsername: process.env.OPENCODE_USERNAME ?? "opencode",
  opencodeServerPassword: requireSecret(
    "OPENCODE_SERVER_PASSWORD",
    process.env.OPENCODE_SERVER_PASSWORD,
  ),
  // MCP endpoint: on by default, bound to localhost. exec-lua / write-file are as
  // powerful as the linked computer, so the listener stays off the public gateway port.
  mcpEnabled: process.env.MCP_ENABLED !== "0",
  mcpHost: process.env.MCP_HOST ?? "127.0.0.1",
  mcpPort: readPort(process.env.MCP_PORT, 4445),
  mcpDefaultTimeoutMs: readPositiveInt(process.env.MCP_DEFAULT_TIMEOUT_MS, 30_000),
  mcpMaxTimeoutMs: readPositiveInt(process.env.MCP_MAX_TIMEOUT_MS, 30_000),
  mcpProbeTimeoutMs: readPositiveInt(process.env.MCP_PROBE_TIMEOUT_MS, 5_000),
  logger: buildLogger(),
};

guardMcpBind(config.mcpEnabled, config.mcpHost);

const { app, mcpApp } = await createApp(config);

try {
  await app.listen({ host: config.host, port: config.port });
} catch (err) {
  reportListenError(err, config.host, config.port, "TRAPOS_SERVER_PORT");
  app.log.error(err);
  process.exit(1);
}

if (mcpApp) {
  try {
    await mcpApp.listen({ host: config.mcpHost, port: config.mcpPort });
  } catch (err) {
    reportListenError(err, config.mcpHost, config.mcpPort, "MCP_PORT");
    mcpApp.log.error(err);
    process.exit(1);
  }
}

function buildLogger(): Config["logger"] {
  const level = process.env.LOG_LEVEL ?? "info";
  if (process.env.LOG_PRETTY) {
    return {
      level,
      transport: {
        target: "pino-pretty",
        options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" },
      },
    };
  }
  return { level };
}

// A non-empty password is mandatory: an empty TRAPOS_SERVER_PASSWORD disables auth
// on the public gateway (resolveNetwork maps any secret to "global"), and an empty
// OPENCODE_SERVER_PASSWORD drops the outbound Basic-auth header. Refuse to start
// rather than run silently unauthenticated.
function requireSecret(name: string, value: string | undefined): string {
  const secret = value?.trim();
  if (!secret) {
    process.stderr.write(
      `\n[trapos-server] ${name} is empty or unset. Set it in .env ` +
        `(run \`just install\` to generate secrets). Refusing to start.\n\n`,
    );
    process.exit(1);
  }
  return secret;
}

// The MCP endpoint (exec-lua / write-file) has no auth of its own — its only trust
// boundary is the loopback binding. A non-loopback MCP_HOST exposes those tools to the
// network unauthenticated, so refuse to start unless the operator explicitly opts in
// with MCP_ALLOW_PUBLIC=1 (e.g. when fronting it with their own authenticated proxy).
function guardMcpBind(enabled: boolean, host: string): void {
  if (!enabled || isLoopbackHost(host) || process.env.MCP_ALLOW_PUBLIC === "1") {
    return;
  }
  process.stderr.write(
    `\n[trapos-server] MCP_HOST is "${host}", not a loopback address. The MCP endpoint ` +
      `(exec-lua / write-file) is UNAUTHENTICATED and would be exposed to the network. ` +
      `Set MCP_HOST=127.0.0.1, disable it with MCP_ENABLED=0, or — if it sits behind your ` +
      `own authenticated proxy — set MCP_ALLOW_PUBLIC=1. Refusing to start.\n\n`,
  );
  process.exit(1);
}

function isLoopbackHost(host: string): boolean {
  return host === "127.0.0.1" || host === "::1" || host === "localhost" || host.startsWith("127.");
}

function readPort(value: string | undefined, fallback: number): number {
  return readPositiveInt(value, fallback);
}

function readPositiveInt(value: string | undefined, fallback: number): number {
  if (!value) {
    return fallback;
  }
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
  return err instanceof Error && "code" in err;
}

function reportListenError(err: unknown, host: string, port: number, portEnv: string): void {
  if (isErrnoException(err) && err.code === "EADDRINUSE") {
    process.stderr.write(
      `\n[trapos-server] Port ${port} is already in use on ${host}.\n` +
        `Another instance may be running, or set ${portEnv} to a free port.\n\n`,
    );
    process.exit(1);
  }
}
