import { createReadStream, existsSync, statSync } from "node:fs";
import { extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import argon2 from "argon2";
import cookie from "@fastify/cookie";
import rateLimit from "@fastify/rate-limit";
import websocket from "@fastify/websocket";
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
import { z } from "zod";
import type { AppConfig } from "./config.js";
import { openDatabase, type CompanionDatabase } from "./database.js";
import { JobRunner } from "./jobs.js";
import {
  clientAddress,
  CSRF_COOKIE,
  hashToken,
  randomToken,
  safeEqual,
  SESSION_COOKIE
} from "./security.js";
import { collectSystemStatus } from "./status.js";
import { readNetworkActivity } from "./network-activity.js";
import { readUpdateStatus, triggerSystemUpdate } from "./update.js";
import {
  hotspotConfigurationSchema,
  networkActionSchema,
  readNetworkStatus,
  requestHotspotConfiguration,
  requestNetworkAction
} from "./network.js";
import {
  collectAdbStatus,
  installAdbApk,
  launchAdbTarget,
  listInstalledAdbPackages,
  MAX_APK_BYTES
} from "./adb.js";
import { CarTelemetryRecorder, type TelemetryPeriod } from "./car-telemetry.js";
import { openShell } from "./shell.js";
import { systemPowerActionSchema, triggerSystemPower } from "./system.js";
import { isLoopbackAddress, registerMobileRoutes } from "./mobile.js";
import "./types.js";

const usernameSchema = z
  .string()
  .trim()
  .min(3, "Username must contain at least 3 characters")
  .max(48, "Username must contain at most 48 characters")
  .regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/, "Use letters, numbers, dots, hyphens, or underscores");

const loginCredentialsSchema = z.object({
  username: usernameSchema,
  password: z.string().min(1).max(256)
});

const setupCredentialsSchema = loginCredentialsSchema.extend({
  password: z
    .string()
    .min(8, "Password must contain at least 8 characters")
    .max(256)
    .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
    .regex(/[0-9]/, "Password must contain at least one number")
});

const adbPackageNameSchema = z.string().trim().max(240).regex(
  /^(?:[a-zA-Z][a-zA-Z0-9_]*\.)+[a-zA-Z][a-zA-Z0-9_]*$/,
  "Enter a valid Android package name"
);
const adbComponentSchema = z.string().trim().max(240).regex(
  /^(?:[a-zA-Z][a-zA-Z0-9_]*\.)+[a-zA-Z][a-zA-Z0-9_]*\/(?:\.|[a-zA-Z])[a-zA-Z0-9_.$]*$/,
  "Enter a valid Android component"
);
const adbTargetSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("component"), value: adbComponentSchema }),
  z.object({ type: z.literal("package"), value: adbPackageNameSchema })
]);
const adbShortcutSchema = z.object({ name: z.string().trim().min(1).max(48), type: z.enum(["component", "package"]), value: z.string() })
  .transform((value, context) => {
    const target = adbTargetSchema.safeParse({ type: value.type, value: value.value });
    if (!target.success) {
      context.addIssue({ code: "custom", message: target.error.issues[0]?.message ?? "Enter a valid Android target" });
      return z.NEVER;
    }
    return { name: value.name, ...target.data };
  });
const clearTelemetrySchema = z.object({ mode: z.enum(["history", "all"]) });

type UserRow = { id: number; username: string; role: "admin"; password_hash: string };
type SessionUserRow = { id: number; username: string; role: "admin" };

function audit(
  database: CompanionDatabase,
  event: {
    actorUserId?: number;
    action: string;
    result: "success" | "failure";
    ipAddress?: string;
    details?: Record<string, string | number | boolean>;
  }
): void {
  database
    .prepare(
      `INSERT INTO audit_events
        (actor_user_id, action, result, ip_address, details_json, created_at)
       VALUES (?, ?, ?, ?, ?, ?)`
    )
    .run(
      event.actorUserId ?? null,
      event.action,
      event.result,
      event.ipAddress ?? null,
      JSON.stringify(event.details ?? {}),
      new Date().toISOString()
    );
}

function cookieOptions(config: AppConfig, httpOnly: boolean) {
  return {
    path: "/",
    httpOnly,
    sameSite: "strict" as const,
    secure: config.cookieSecure
  };
}

function requireUser(request: FastifyRequest, reply: FastifyReply): boolean {
  if (request.authUser) return true;
  void reply.code(401).send({ error: { code: "AUTH_REQUIRED", message: "Sign in to continue" } });
  return false;
}

export async function buildApp(config: AppConfig): Promise<FastifyInstance> {
  const app = Fastify({
    logger:
      config.nodeEnv === "test"
        ? false
        : {
            level: config.nodeEnv === "production" ? "info" : "debug",
            redact: ["req.headers.cookie", "req.headers.authorization", "req.headers.x-csrf-token"]
          }
  });
  const database = openDatabase(config.databasePath);
  const jobRunner = new JobRunner(database, undefined, config.adb);
  const carTelemetry = new CarTelemetryRecorder(database, config.adb);
  if (config.adb.enabled) void collectAdbStatus(config.adb);
  carTelemetry.start();

  app.decorate("database", database);
  app.decorate("jobRunner", jobRunner);
  app.decorateRequest("authUser", null);
  await app.register(cookie);
  await app.register(rateLimit, { global: false });
  await app.register(websocket);
  app.addContentTypeParser(
    ["application/vnd.android.package-archive", "application/octet-stream"],
    { parseAs: "buffer" },
    (_request, body, done) => done(null, body)
  );

  app.addHook("onClose", async () => {
    carTelemetry.stop();
    jobRunner.close();
    database.close();
  });
  app.addHook("onSend", async (_request, reply) => {
    reply.header("X-Content-Type-Options", "nosniff");
    reply.header("Referrer-Policy", "no-referrer");
    reply.header("X-Frame-Options", "DENY");
    reply.header(
      "Content-Security-Policy",
      "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self'"
    );
  });

  app.addHook("onRequest", async (request) => {
    request.authUser = null;
    const token = request.cookies[SESSION_COOKIE];
    if (!token) return;

    const now = new Date().toISOString();
    const user = database
      .prepare(
        `SELECT users.id, users.username, users.role
         FROM sessions JOIN users ON users.id = sessions.user_id
         WHERE sessions.id_hash = ? AND sessions.expires_at > ?`
      )
      .get(hashToken(token), now) as SessionUserRow | undefined;
    if (user) request.authUser = user;
  });

  app.addHook("preValidation", async (request, reply) => {
    if (!["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) return;
    if (request.url.startsWith("/internal/mobile/") && isLoopbackAddress(request.ip)) return;

    const origin = request.headers.origin;
    let sameHostOrigin = false;
    if (origin && request.headers.host) {
      try {
        const parsedOrigin = new URL(origin);
        sameHostOrigin = ["http:", "https:"].includes(parsedOrigin.protocol) && parsedOrigin.host === request.headers.host;
      } catch {
        sameHostOrigin = false;
      }
    }
    if (origin && !sameHostOrigin && !config.allowedOrigins.has(origin)) {
      return reply.code(403).send({ error: { code: "ORIGIN_REJECTED", message: "Request origin is not trusted" } });
    }

    const cookieToken = request.cookies[CSRF_COOKIE];
    const headerToken = request.headers["x-csrf-token"];
    if (!cookieToken || typeof headerToken !== "string" || !safeEqual(cookieToken, headerToken)) {
      return reply.code(403).send({ error: { code: "CSRF_REJECTED", message: "Refresh the page and try again" } });
    }
  });

  app.get("/healthz", async () => ({ status: "ok", timestamp: new Date().toISOString() }));

  app.get("/api/auth/state", async (request, reply) => {
    let csrfToken = request.cookies[CSRF_COOKIE];
    if (!csrfToken) {
      csrfToken = randomToken();
      reply.setCookie(CSRF_COOKIE, csrfToken, cookieOptions(config, false));
    }
    const row = database.prepare("SELECT COUNT(*) AS count FROM users").get() as { count: number };
    return {
      setupRequired: row.count === 0,
      user: request.authUser,
      csrfToken
    };
  });

  app.post(
    "/api/auth/setup",
    { config: { rateLimit: { max: 5, timeWindow: "1 minute" } } },
    async (request, reply) => {
      const parsed = setupCredentialsSchema.safeParse(request.body);
      if (!parsed.success) {
        return reply.code(400).send({
          error: { code: "INVALID_CREDENTIALS", message: parsed.error.issues[0]?.message ?? "Invalid account details" }
        });
      }

      const passwordHash = await argon2.hash(parsed.data.password, { type: argon2.argon2id });
      const createAdmin = database.transaction(() => {
        const row = database.prepare("SELECT COUNT(*) AS count FROM users").get() as { count: number };
        if (row.count !== 0) return null;
        const result = database
          .prepare("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, 'admin', ?)")
          .run(parsed.data.username, passwordHash, new Date().toISOString());
        return Number(result.lastInsertRowid);
      });
      const userId = createAdmin();
      if (userId === null) {
        audit(database, {
          action: "auth.setup",
          result: "failure",
          ipAddress: clientAddress(request),
          details: { reason: "setup_already_complete" }
        });
        return reply.code(409).send({ error: { code: "SETUP_COMPLETE", message: "Initial setup is already complete" } });
      }

      audit(database, {
        actorUserId: userId,
        action: "auth.setup",
        result: "success",
        ipAddress: clientAddress(request)
      });
      return reply.code(201).send({ created: true });
    }
  );

  app.post(
    "/api/auth/login",
    { config: { rateLimit: { max: 8, timeWindow: "1 minute" } } },
    async (request, reply) => {
      const parsed = loginCredentialsSchema.safeParse(request.body);
      const user = parsed.success
        ? (database
            .prepare("SELECT id, username, role, password_hash FROM users WHERE username = ? COLLATE NOCASE")
            .get(parsed.data.username) as UserRow | undefined)
        : undefined;
      const valid = user && parsed.success ? await argon2.verify(user.password_hash, parsed.data.password) : false;

      if (!user || !valid) {
        audit(database, {
          action: "auth.login",
          result: "failure",
          ipAddress: clientAddress(request),
          details: { reason: "invalid_credentials" }
        });
        return reply.code(401).send({ error: { code: "LOGIN_FAILED", message: "Username or password is incorrect" } });
      }

      database.prepare("DELETE FROM sessions WHERE expires_at <= ?").run(new Date().toISOString());
      const token = randomToken();
      const expiresAt = new Date(Date.now() + config.sessionTtlMs);
      database
        .prepare("INSERT INTO sessions (id_hash, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)")
        .run(hashToken(token), user.id, expiresAt.toISOString(), new Date().toISOString());
      reply.setCookie(SESSION_COOKIE, token, {
        ...cookieOptions(config, true),
        expires: expiresAt
      });
      audit(database, {
        actorUserId: user.id,
        action: "auth.login",
        result: "success",
        ipAddress: clientAddress(request)
      });
      return { user: { id: user.id, username: user.username, role: user.role } };
    }
  );

  app.post("/api/auth/logout", async (request, reply) => {
    const token = request.cookies[SESSION_COOKIE];
    if (token) database.prepare("DELETE FROM sessions WHERE id_hash = ?").run(hashToken(token));
    if (request.authUser) {
      audit(database, {
        actorUserId: request.authUser.id,
        action: "auth.logout",
        result: "success",
        ipAddress: clientAddress(request)
      });
    }
    reply.clearCookie(SESSION_COOKIE, cookieOptions(config, true));
    return { loggedOut: true };
  });

  registerMobileRoutes(app, database, config.adb, carTelemetry, requireUser);

  app.get("/api/status", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return collectSystemStatus(config.adb);
  });

  app.get("/api/jobs", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return { jobs: jobRunner.listJobs() };
  });

  app.get("/api/job-runs", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return { runs: jobRunner.listRuns() };
  });

  app.get<{ Params: { id: string } }>("/api/job-runs/:id", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const run = jobRunner.getRun(request.params.id);
    if (!run) return reply.code(404).send({ error: { code: "RUN_NOT_FOUND", message: "Job run not found" } });
    return run;
  });

  app.post<{ Params: { jobId: string } }>("/api/jobs/:jobId/runs", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    if (!jobRunner.has(request.params.jobId)) {
      return reply.code(404).send({ error: { code: "JOB_NOT_FOUND", message: "Job is not available" } });
    }
    const run = jobRunner.enqueue(request.params.jobId, request.authUser!.id);
    audit(database, {
      actorUserId: request.authUser!.id,
      action: "job.enqueue",
      result: "success",
      ipAddress: clientAddress(request),
      details: { jobId: request.params.jobId, runId: run.id }
    });
    return reply.code(202).send(run);
  });

  app.get("/api/network", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return readNetworkStatus(config);
  });

  app.get("/api/network/activity", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return readNetworkActivity(config.network.activityPath);
  });

  app.get("/api/adb/packages", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    try {
      return { packages: await listInstalledAdbPackages(config.adb) };
    } catch {
      return reply.code(503).send({ error: { code: "ADB_UNAVAILABLE", message: "The infotainment unit is not available over ADB" } });
    }
  });

  app.post(
    "/api/adb/install",
    { config: { rateLimit: { max: 4, timeWindow: "10 minutes" } }, bodyLimit: MAX_APK_BYTES },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      if (!Buffer.isBuffer(request.body)) {
        return reply.code(400).send({ error: { code: "INVALID_APK", message: "Choose a valid APK file" } });
      }
      try {
        await installAdbApk(config.adb, request.body);
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "adb.install-apk",
          result: "success",
          ipAddress: clientAddress(request),
          details: { bytes: request.body.byteLength }
        });
        return reply.code(201).send({ installed: true });
      } catch (error) {
        const invalid = error instanceof Error && error.message === "INVALID_APK";
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "adb.install-apk",
          result: "failure",
          ipAddress: clientAddress(request),
          details: { reason: invalid ? "invalid_apk" : "install_failed" }
        });
        return reply.code(invalid ? 400 : 503).send({
          error: {
            code: invalid ? "INVALID_APK" : "APK_INSTALL_FAILED",
            message: invalid ? "Choose a valid APK file" : "The APK could not be installed on the infotainment unit"
          }
        });
      }
    }
  );

  app.post("/api/adb/launch", { config: { rateLimit: { max: 30, timeWindow: "1 minute" } } }, async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const parsed = adbTargetSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: { code: "INVALID_ADB_TARGET", message: "Enter a valid package or activity" } });
    }
    try {
      await launchAdbTarget(config.adb, parsed.data);
      audit(database, {
        actorUserId: request.authUser!.id,
        action: "adb.launch",
        result: "success",
        ipAddress: clientAddress(request),
        details: { targetType: parsed.data.type, target: parsed.data.value }
      });
      return { launched: true };
    } catch {
      audit(database, {
        actorUserId: request.authUser!.id,
        action: "adb.launch",
        result: "failure",
        ipAddress: clientAddress(request),
        details: { targetType: parsed.data.type, target: parsed.data.value }
      });
      return reply.code(503).send({ error: { code: "ADB_LAUNCH_FAILED", message: "The target could not be opened on the infotainment unit" } });
    }
  });

  app.get("/api/adb/shortcuts", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const shortcuts = database
      .prepare("SELECT id, name, target_type AS type, target_value AS value, created_at AS createdAt FROM adb_shortcuts ORDER BY created_at, id")
      .all();
    return { shortcuts };
  });

  app.post("/api/adb/shortcuts", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const parsed = adbShortcutSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: { code: "INVALID_ADB_SHORTCUT", message: "Enter a name and a valid target" } });
    }
    try {
      const createdAt = new Date().toISOString();
      const result = database
        .prepare("INSERT INTO adb_shortcuts (name, target_type, target_value, created_at) VALUES (?, ?, ?, ?)")
        .run(parsed.data.name, parsed.data.type, parsed.data.value, createdAt);
      return reply.code(201).send({ id: Number(result.lastInsertRowid), ...parsed.data, createdAt });
    } catch {
      return reply.code(409).send({ error: { code: "ADB_SHORTCUT_EXISTS", message: "A shortcut for that target already exists" } });
    }
  });

  app.delete<{ Params: { id: string } }>("/api/adb/shortcuts/:id", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const id = Number(request.params.id);
    if (!Number.isInteger(id) || id < 1) {
      return reply.code(400).send({ error: { code: "INVALID_ADB_SHORTCUT", message: "That shortcut is invalid" } });
    }
    const result = database.prepare("DELETE FROM adb_shortcuts WHERE id = ?").run(id);
    if (result.changes === 0) return reply.code(404).send({ error: { code: "ADB_SHORTCUT_NOT_FOUND", message: "Shortcut not found" } });
    return reply.code(204).send();
  });

  app.get("/api/car/telemetry", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    try {
      return await carTelemetry.readLive();
    } catch {
      return reply.code(503).send({ error: { code: "CAR_TELEMETRY_UNAVAILABLE", message: "Live car telemetry is unavailable over ADB" } });
    }
  });

  app.get<{ Querystring: { period?: string } }>("/api/car/history", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const parsed = z.enum(["24h", "7d", "30d", "1y", "all"]).safeParse(request.query.period ?? "24h");
    if (!parsed.success) {
      return reply.code(400).send({ error: { code: "INVALID_HISTORY_PERIOD", message: "That history period is not available" } });
    }
    return carTelemetry.store.history(parsed.data as TelemetryPeriod);
  });

  app.get("/api/car/statistics", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return carTelemetry.store.overview();
  });

  app.post(
    "/api/car/history/clear",
    { config: { rateLimit: { max: 4, timeWindow: "10 minutes" } } },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      const parsed = clearTelemetrySchema.safeParse(request.body);
      if (!parsed.success) {
        return reply.code(400).send({
          error: { code: "INVALID_CLEAR_TELEMETRY_MODE", message: "Choose which car statistics to clear" }
        });
      }

      const result = carTelemetry.store.clear(parsed.data.mode);
      audit(database, {
        actorUserId: request.authUser!.id,
        action: "car.telemetry-clear",
        result: "success",
        ipAddress: clientAddress(request),
        details: result
      });
      return result;
    }
  );

  app.post(
    "/api/network/configuration",
    { config: { rateLimit: { max: 3, timeWindow: "10 minutes" } } },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      const parsed = hotspotConfigurationSchema.safeParse(request.body);
      if (!parsed.success) {
        return reply.code(400).send({
          error: {
            code: "INVALID_HOTSPOT_CONFIGURATION",
            message: parsed.error.issues[0]?.message ?? "The hotspot settings are invalid"
          }
        });
      }
      try {
        const configuration = await requestHotspotConfiguration(config, parsed.data, request.authUser!.id);
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "network.configure-hotspot",
          result: "success",
          ipAddress: clientAddress(request),
          details: { requestId: configuration.id }
        });
        return reply.code(202).send({ accepted: true, requestId: configuration.id });
      } catch {
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "network.configure-hotspot",
          result: "failure",
          ipAddress: clientAddress(request),
          details: { reason: "configuration_service_unavailable" }
        });
        return reply.code(503).send({
          error: { code: "HOTSPOT_CONFIGURATION_UNAVAILABLE", message: "The hotspot configuration service is unavailable" }
        });
      }
    }
  );

  app.post("/api/network/actions", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    const parsed = z.object({ action: networkActionSchema }).safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: { code: "INVALID_NETWORK_ACTION", message: "That network action is not available" } });
    }
    try {
      const command = await requestNetworkAction(config, parsed.data.action, request.authUser!.id);
      audit(database, {
        actorUserId: request.authUser!.id,
        action: `network.${parsed.data.action}`,
        result: "success",
        ipAddress: clientAddress(request),
        details: { commandId: command.id }
      });
      return reply.code(202).send({ accepted: true, commandId: command.id });
    } catch {
      audit(database, {
        actorUserId: request.authUser!.id,
        action: `network.${parsed.data.action}`,
        result: "failure",
        ipAddress: clientAddress(request),
        details: { reason: "controller_unavailable" }
      });
      return reply.code(503).send({
        error: { code: "NETWORK_CONTROL_UNAVAILABLE", message: "Automatic network management is not configured" }
      });
    }
  });

  app.get("/api/system/update", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    return readUpdateStatus(config);
  });

  app.post(
    "/api/system/update/check",
    { config: { rateLimit: { max: 6, timeWindow: "10 minutes" } } },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      const current = await readUpdateStatus(config);
      if (!current.supported) {
        return reply.code(503).send({
          error: { code: "UPDATES_UNAVAILABLE", message: "Install the system service before checking for updates" }
        });
      }
      if (["checking", "building"].includes(current.state)) {
        return reply.code(409).send({ error: { code: "UPDATE_IN_PROGRESS", message: "An update task is already running" } });
      }
      try {
        await triggerSystemUpdate("check");
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "system.update-check",
          result: "success",
          ipAddress: clientAddress(request)
        });
        return reply.code(202).send({ queued: true });
      } catch {
        return reply.code(503).send({
          error: { code: "UPDATE_CHECK_FAILED", message: "The update check could not be started" }
        });
      }
    }
  );

  app.post(
    "/api/system/update",
    { config: { rateLimit: { max: 2, timeWindow: "10 minutes" } } },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      const current = await readUpdateStatus(config);
      if (!current.supported) {
        return reply.code(503).send({
          error: { code: "UPDATES_UNAVAILABLE", message: "Install the system service before using automatic updates" }
        });
      }
      if (["checking", "building"].includes(current.state)) {
        return reply.code(409).send({ error: { code: "UPDATE_IN_PROGRESS", message: "An update is already running" } });
      }

      try {
        await triggerSystemUpdate("install");
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "system.update",
          result: "success",
          ipAddress: clientAddress(request),
          details: { state: "queued" }
        });
        return reply.code(202).send({ queued: true });
      } catch {
        audit(database, {
          actorUserId: request.authUser!.id,
          action: "system.update",
          result: "failure",
          ipAddress: clientAddress(request),
          details: { reason: "update_service_unavailable" }
        });
        return reply.code(503).send({
          error: { code: "UPDATE_START_FAILED", message: "The update service could not be started" }
        });
      }
    }
  );

  app.post(
    "/api/system/power",
    { config: { rateLimit: { max: 3, timeWindow: "10 minutes" } } },
    async (request, reply) => {
      if (!requireUser(request, reply)) return;
      const parsed = z.object({ action: systemPowerActionSchema }).safeParse(request.body);
      if (!parsed.success) {
        return reply.code(400).send({ error: { code: "INVALID_POWER_ACTION", message: "That power action is not available" } });
      }
      try {
        await triggerSystemPower(parsed.data.action);
        audit(database, {
          actorUserId: request.authUser!.id,
          action: `system.${parsed.data.action}`,
          result: "success",
          ipAddress: clientAddress(request)
        });
        return reply.code(202).send({ queued: true, action: parsed.data.action });
      } catch {
        audit(database, {
          actorUserId: request.authUser!.id,
          action: `system.${parsed.data.action}`,
          result: "failure",
          ipAddress: clientAddress(request)
        });
        return reply.code(503).send({
          error: { code: "POWER_ACTION_FAILED", message: "The Pi could not schedule that power action" }
        });
      }
    }
  );

  app.get("/api/system/shell", { websocket: true }, (socket, request) => {
    openShell(socket, request, config);
  });

  app.get("/api/events", async (request, reply) => {
    if (!requireUser(request, reply)) return;
    reply.hijack();
    reply.raw.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no"
    });
    let closed = false;
    const sendStatus = async () => {
      if (closed) return;
      try {
        const status = await collectSystemStatus(config.adb);
        reply.raw.write(`event: status\ndata: ${JSON.stringify(status)}\n\n`);
      } catch {
        reply.raw.write(`event: collection-error\ndata: {"code":"STATUS_COLLECTION_FAILED"}\n\n`);
      }
    };
    await sendStatus();
    const interval = setInterval(() => void sendStatus(), 10_000);
    const stopJobEvents = jobRunner.onRun((run) => {
      if (!closed) reply.raw.write(`event: job-run\ndata: ${JSON.stringify(run)}\n\n`);
    });
    request.raw.on("close", () => {
      closed = true;
      clearInterval(interval);
      stopJobEvents();
    });
  });

  const webRoot = resolve(fileURLToPath(new URL("../../web/dist", import.meta.url)));
  if (config.nodeEnv === "production" && existsSync(webRoot)) {
    app.get("/", async (_request, reply) => {
      reply.type("text/html; charset=utf-8");
      return reply.send(createReadStream(resolve(webRoot, "index.html")));
    });
    const publicFiles: Record<string, { file: string; type: string; cacheControl: string }> = {
      "/manifest.webmanifest": {
        file: "manifest.webmanifest",
        type: "application/manifest+json; charset=utf-8",
        cacheControl: "no-cache"
      },
      "/service-worker.js": {
        file: "service-worker.js",
        type: "text/javascript; charset=utf-8",
        cacheControl: "no-cache"
      },
      "/icon.svg": { file: "icon.svg", type: "image/svg+xml", cacheControl: "public, max-age=86400" },
      "/pwa-192x192.png": { file: "pwa-192x192.png", type: "image/png", cacheControl: "public, max-age=86400" },
      "/pwa-512x512.png": { file: "pwa-512x512.png", type: "image/png", cacheControl: "public, max-age=86400" },
      "/pwa-maskable-512x512.png": {
        file: "pwa-maskable-512x512.png",
        type: "image/png",
        cacheControl: "public, max-age=86400"
      },
      "/apple-touch-icon.png": {
        file: "apple-touch-icon.png",
        type: "image/png",
        cacheControl: "public, max-age=86400"
      }
    };
    for (const [route, asset] of Object.entries(publicFiles)) {
      app.get(route, async (_request, reply) => {
        const assetPath = resolve(webRoot, asset.file);
        if (!existsSync(assetPath) || !statSync(assetPath).isFile()) {
          return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } });
        }
        reply.type(asset.type);
        reply.header("Cache-Control", asset.cacheControl);
        return reply.send(createReadStream(assetPath));
      });
    }
    app.get<{ Params: { file: string } }>("/assets/:file", async (request, reply) => {
      if (!/^[a-zA-Z0-9._-]+$/.test(request.params.file)) {
        return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } });
      }
      const assetPath = resolve(webRoot, "assets", request.params.file);
      if (!existsSync(assetPath) || !statSync(assetPath).isFile()) {
        return reply.code(404).send({ error: { code: "NOT_FOUND", message: "Asset not found" } });
      }
      const contentTypes: Record<string, string> = {
        ".css": "text/css; charset=utf-8",
        ".js": "text/javascript; charset=utf-8",
        ".map": "application/json; charset=utf-8",
        ".png": "image/png",
        ".svg": "image/svg+xml",
        ".woff2": "font/woff2"
      };
      reply.type(contentTypes[extname(assetPath)] ?? "application/octet-stream");
      reply.header("Cache-Control", "public, max-age=31536000, immutable");
      return reply.send(createReadStream(assetPath));
    });
  }

  return app;
}
