import { randomUUID } from "node:crypto";
import { createReadStream } from "node:fs";
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
import type { Server } from "node:http";
import path from "node:path";
import express, { type NextFunction, type Request, type Response } from "express";
import multer from "multer";

import { TaskEngine } from "./agent/engine.ts";
import { config } from "./config.ts";
import { json, openDatabase, parseJson, type Db } from "./db/index.ts";
import {
  checkpointTask, createProjectEntry, deleteProjectEntry, FileServiceError, listProjectFiles,
  projectFileContent, projectGitStatus, revertTaskChanges, taskChangedFiles, taskDiff,
  taskDiffVersion, updateProjectEntry, uploadProjectAsset,
} from "./files.ts";
import {
  browseProjectTree, discoverProjects, getProjectById, projectDetails,
  recentProjects, recordProjectActivity, validateWorkspacePath,
} from "./projects.ts";
import {
  audit, clearSessionCookie, createSession, hashPassword, ipHash,
  issuePreauth, requireAuth, requireCsrf, securityHeaders,
  sessionMiddleware, validatePassword, verifyPassword,
} from "./security.ts";
import type { AuthenticatedRequest } from "./types.ts";

const ok = (response: Response, data: unknown, status = 200) => response.status(status).json({ ok: true, data });
const fail = (response: Response, status: number, code: string, message: string, details?: unknown) => response.status(status).json({ ok: false, error: { code, message, ...(details ? { details } : {}) } });
const text = (value: unknown, max = 100_000) => typeof value === "string" ? value.trim().slice(0, max) : "";
const own = (request: AuthenticatedRequest) => request.auth!.userId;
const now = () => new Date().toISOString();

function fileError(response: Response, error: unknown) {
  if (error instanceof FileServiceError) return fail(response, error.status, error.code, error.message, error.details);
  throw error;
}

function asyncRoute(handler: (request: AuthenticatedRequest, response: Response, next: NextFunction) => Promise<unknown>) {
  return (request: AuthenticatedRequest, response: Response, next: NextFunction) => { void handler(request, response, next).catch(next); };
}

function threadForUser(db: Db, id: string, userId: string) {
  return db.prepare("SELECT * FROM threads WHERE id = ? AND user_id = ?").get(id, userId) as any;
}

function taskForUser(db: Db, id: string, userId: string) {
  return db.prepare("SELECT t.* FROM tasks t JOIN threads th ON th.id=t.thread_id WHERE t.id=? AND th.user_id=?").get(id, userId) as any;
}

function serializeThread(row: any) {
  return { id: row.id, projectId: row.project_id, projectPath: row.project_path, title: row.title, archived: Boolean(row.archived), createdAt: row.created_at, updatedAt: row.updated_at, lastActivityAt: row.last_activity_at };
}

function serializeMessage(row: any) {
  return { id: row.id, threadId: row.thread_id, role: row.role, content: row.content, metadata: parseJson(row.metadata_json, {}), completionState: row.completion_state, taskId: row.task_id, parentMessageId: row.parent_message_id, createdAt: row.created_at, updatedAt: row.updated_at };
}

const loginAttempts = new Map<string, { count: number; resetAt: number; lockedUntil?: number }>();

function checkLoginRate(request: Request) {
  const key = ipHash(request);
  const current = loginAttempts.get(key);
  if (!current || current.resetAt <= Date.now()) return { key, state: { count: 0, resetAt: Date.now() + config.loginWindowSeconds * 1000 } };
  return { key, state: current };
}

const allowedUploads = new Map([
  [".png", ["image/png"]], [".jpg", ["image/jpeg"]], [".jpeg", ["image/jpeg"]],
  [".webp", ["image/webp"]], [".gif", ["image/gif"]], [".txt", ["text/plain"]],
  [".json", ["application/json", "text/json"]], [".csv", ["text/csv", "application/csv"]],
  [".md", ["text/markdown", "text/plain"]], [".pdf", ["application/pdf"]],
]);
const forbiddenNameParts = /(?:^\.|\.php\d*\.|\.phar\.|\.phtml\.|\.sh\.|\.bash\.|\.htaccess|\.user\.ini|php\.ini)/i;

function validateUpload(file: Express.Multer.File) {
  const name = path.basename(file.originalname).normalize("NFKC");
  const extension = path.extname(name).toLowerCase();
  if (!name || name.startsWith(".") || forbiddenNameParts.test(name) || name.split(".").length > 2) throw new Error("UPLOAD_REJECTED");
  const media = allowedUploads.get(extension);
  if (!media?.includes(file.mimetype.toLowerCase())) throw new Error("UPLOAD_REJECTED");
  return { name: name.slice(0, 200), mediaType: file.mimetype.toLowerCase() };
}

export async function createApp(options: { databasePath?: string } = {}) {
  const db = await openDatabase(options.databasePath);
  const engine = new TaskEngine(db, config.agentProvider);
  const app = express();
  app.disable("x-powered-by");
  if (process.env.TRUST_PROXY === "true") app.set("trust proxy", 1);
  app.use(securityHeaders);
  app.use(express.json({ limit: config.requestLimit }));
  app.use(express.urlencoded({ extended: false, limit: config.requestLimit }));
  app.use(sessionMiddleware(db));

  app.get("/api/health", (_request, response) => ok(response, { status: "ok", version: "0.2.0", time: now() }));

  app.get("/api/auth/session", (request: AuthenticatedRequest, response) => {
    const setupRequired = Number((db.prepare("SELECT COUNT(*) count FROM users").get() as any).count) === 0;
    if (!request.auth) {
      const csrfToken = issuePreauth(response);
      return ok(response, { authenticated: false, setupRequired, expired: Boolean(response.locals.sessionExpired), csrfToken });
    }
    return ok(response, { authenticated: true, setupRequired: false, csrfToken: request.auth.csrfToken, expiresAt: request.auth.expiresAt, user: { id: request.auth.userId, username: request.auth.username, role: "admin" } });
  });

  app.post("/api/auth/setup", requireCsrf, asyncRoute(async (request, response) => {
    if (Number((db.prepare("SELECT COUNT(*) count FROM users").get() as any).count) > 0) return fail(response, 409, "CONFLICT", "Administrator setup is already complete.");
    const username = text(request.body.username, 64);
    const password = typeof request.body.password === "string" ? request.body.password : "";
    if (!/^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$/.test(username)) return fail(response, 422, "VALIDATION_ERROR", "Use 3–64 letters, numbers, dots, underscores, or hyphens for the username.");
    if (password !== request.body.passwordConfirmation) return fail(response, 422, "VALIDATION_ERROR", "Passwords do not match.");
    const passwordErrors = validatePassword(password);
    if (passwordErrors.length) return fail(response, 422, "VALIDATION_ERROR", "The password does not meet the security requirements.", passwordErrors);
    const id = randomUUID();
    const passwordHash = await hashPassword(password);
    db.prepare("INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)").run(id, username, passwordHash);
    db.prepare("INSERT INTO settings (user_id) VALUES (?)").run(id);
    const session = createSession(db, { id }, request, response);
    response.clearCookie(config.preauthCookie, { path: "/" });
    audit(db, request, "auth.setup", "user", id);
    return ok(response, { user: { id, username, role: "admin" }, csrfToken: session.csrfToken, expiresAt: session.expiresAt }, 201);
  }));

  app.post("/api/auth/login", requireCsrf, asyncRoute(async (request, response) => {
    const rate = checkLoginRate(request);
    if ((rate.state.lockedUntil ?? 0) > Date.now()) return fail(response, 429, "LOCKED", "Too many unsuccessful attempts. Try again later.", { retryAfterSeconds: Math.ceil((rate.state.lockedUntil! - Date.now()) / 1000) });
    const username = text(request.body.username, 64);
    const password = typeof request.body.password === "string" ? request.body.password : "";
    const user = db.prepare("SELECT * FROM users WHERE username = ? COLLATE NOCASE").get(username) as any;
    const valid = await verifyPassword(password, user?.password_hash ?? "$2b$12$C6UzMDM.H6dfI/f/IKcEe.3z4YdTQIa5PHcPIZS6MsBlABGZ2pKMi");
    if (!user || !valid || (user.locked_until && Date.parse(user.locked_until) > Date.now())) {
      rate.state.count += 1;
      if (rate.state.count >= config.loginMaxAttempts) rate.state.lockedUntil = Date.now() + config.lockoutSeconds * 1000;
      loginAttempts.set(rate.key, rate.state);
      if (user) db.prepare("UPDATE users SET login_failures = login_failures + 1, locked_until = CASE WHEN login_failures + 1 >= ? THEN ? ELSE locked_until END WHERE id = ?").run(config.loginMaxAttempts, new Date(Date.now() + config.lockoutSeconds * 1000).toISOString(), user.id);
      audit(db, request, "auth.login_failed", "user", null);
      return fail(response, 401, "AUTH_INVALID", "The username or password is incorrect.");
    }
    loginAttempts.delete(rate.key);
    db.prepare("UPDATE users SET login_failures=0, locked_until=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?").run(user.id);
    const session = createSession(db, user, request, response);
    response.clearCookie(config.preauthCookie, { path: "/" });
    audit(db, request, "auth.login", "user", user.id);
    return ok(response, { user: { id: user.id, username: user.username, role: user.role }, csrfToken: session.csrfToken, expiresAt: session.expiresAt });
  }));

  app.use("/api", requireAuth);
  app.get("/api/version", (request: AuthenticatedRequest, response) => ok(response, { version: "0.2.0", agent: engine.provider === "mock" ? "Development adapter" : "Configured adapter" }));

  app.post("/api/auth/logout", requireCsrf, (request: AuthenticatedRequest, response) => {
    db.prepare("UPDATE sessions SET revoked_at=CURRENT_TIMESTAMP WHERE id=?").run(request.auth!.sessionId);
    audit(db, request, "auth.logout", "session", request.auth!.sessionId);
    clearSessionCookie(response);
    return ok(response, { loggedOut: true });
  });
  app.post("/api/auth/logout-all", requireCsrf, (request: AuthenticatedRequest, response) => {
    db.prepare("UPDATE sessions SET revoked_at=CURRENT_TIMESTAMP WHERE user_id=? AND revoked_at IS NULL").run(own(request));
    audit(db, request, "auth.logout_all", "user", own(request));
    clearSessionCookie(response);
    return ok(response, { loggedOut: true });
  });
  app.post("/api/auth/change-password", requireCsrf, asyncRoute(async (request, response) => {
    const user = db.prepare("SELECT * FROM users WHERE id=?").get(own(request)) as any;
    if (!await verifyPassword(request.body.currentPassword ?? "", user.password_hash)) return fail(response, 422, "VALIDATION_ERROR", "The current password is incorrect.");
    const errors = validatePassword(request.body.newPassword ?? "");
    if (errors.length) return fail(response, 422, "VALIDATION_ERROR", "The new password does not meet the security requirements.", errors);
    db.prepare("UPDATE users SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?").run(await hashPassword(request.body.newPassword), user.id);
    db.prepare("UPDATE sessions SET revoked_at=CURRENT_TIMESTAMP WHERE user_id=? AND id<>?").run(user.id, request.auth!.sessionId);
    audit(db, request, "auth.password_changed", "user", user.id);
    return ok(response, { changed: true });
  }));

  app.get("/api/projects", asyncRoute(async (request, response) => ok(response, await discoverProjects(db, own(request)))));
  app.post("/api/projects/refresh", requireCsrf, asyncRoute(async (request, response) => ok(response, await discoverProjects(db, own(request)))));
  app.get("/api/projects/tree", asyncRoute(async (request, response) => {
    try { return ok(response, await browseProjectTree(db, own(request), text(request.query.projectId, 64))); }
    catch { return fail(response, 422, "PROJECT_INVALID", "The requested folder is outside the allowed workspace."); }
  }));
  app.get("/api/projects/recent", asyncRoute(async (request, response) => ok(response, await recentProjects(db, own(request)))));
  app.post("/api/projects/select", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const project = await getProjectById(db, own(request), text(request.body.projectId, 64));
      db.prepare(`INSERT INTO project_preferences (user_id,project_id,last_selected_at) VALUES (?,?,CURRENT_TIMESTAMP)
        ON CONFLICT(user_id,project_id) DO UPDATE SET last_selected_at=CURRENT_TIMESTAMP`).run(own(request), project.id);
      recordProjectActivity(db, own(request), project.id, "selected", "Opened workspace");
      audit(db, request, "project.selected", "project", project.id, { path: project.path });
      return ok(response, await getProjectById(db, own(request), project.id));
    } catch { return fail(response, 422, "PROJECT_INVALID", "The selected project is outside the allowed workspace."); }
  }));
  app.post("/api/projects/:id/favorite", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const project = await getProjectById(db, own(request), String(request.params.id));
      const favorite = Boolean(request.body.favorite);
      if (favorite) db.prepare("INSERT OR IGNORE INTO project_favorites (user_id,project_id) VALUES (?,?)").run(own(request), project.id);
      else db.prepare("DELETE FROM project_favorites WHERE user_id=? AND project_id=?").run(own(request), project.id);
      db.prepare(`INSERT INTO project_preferences (user_id,project_id,favorite) VALUES (?,?,?)
        ON CONFLICT(user_id,project_id) DO UPDATE SET favorite=excluded.favorite`).run(own(request), project.id, Number(favorite));
      recordProjectActivity(db, own(request), project.id, favorite ? "favorited" : "unfavorited", favorite ? "Added to favorites" : "Removed from favorites");
      return ok(response, { projectId: project.id, favorite });
    } catch { return fail(response, 404, "NOT_FOUND", "Project not found."); }
  }));
  app.patch("/api/projects/:id", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const project = await getProjectById(db, own(request), String(request.params.id));
      const label = text(request.body.label, 81);
      if (!label || label.length > 80) return fail(response, 422, "VALIDATION_ERROR", "Use a project label between 1 and 80 characters.");
      db.prepare(`INSERT INTO project_labels (user_id,project_id,label,updated_at) VALUES (?,?,?,CURRENT_TIMESTAMP)
        ON CONFLICT(user_id,project_id) DO UPDATE SET label=excluded.label,updated_at=CURRENT_TIMESTAMP`).run(own(request), project.id, label);
      db.prepare(`INSERT INTO project_preferences (user_id,project_id,custom_label) VALUES (?,?,?)
        ON CONFLICT(user_id,project_id) DO UPDATE SET custom_label=excluded.custom_label`).run(own(request), project.id, label);
      recordProjectActivity(db, own(request), project.id, "label_updated", "Updated workspace label");
      audit(db, request, "project.label_updated", "project", project.id);
      return ok(response, await getProjectById(db, own(request), project.id));
    } catch { return fail(response, 404, "NOT_FOUND", "Project not found."); }
  }));
  const projectUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: config.uploadMaxBytes, files: 1 } });
  app.get("/api/projects/:id/files", asyncRoute(async (request, response) => {
    try {
      const result = await listProjectFiles(db, own(request), String(request.params.id), {
        directory: text(request.query.path, 1000), search: text(request.query.search, 200),
        textSearch: text(request.query.textSearch, 200), showProtected: request.query.showProtected === "true",
      });
      return ok(response, { ...result, git: await projectGitStatus(db, own(request), String(request.params.id)) });
    } catch (error) { return fileError(response, error); }
  }));
  app.get("/api/projects/:id/files/content", asyncRoute(async (request, response) => {
    try {
      const result = await projectFileContent(db, own(request), String(request.params.id), text(request.query.path, 1000), { download: request.query.download === "true" });
      const raw = request.query.raw === "true" || request.query.download === "true";
      if (raw) {
        response.set({ "Content-Type": result.mediaType ?? "text/plain; charset=utf-8", "Content-Length": String(result.info.size), "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" });
        if (request.query.download === "true") response.setHeader("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(path.basename(result.target.relative))}`);
        return createReadStream(result.target.absolute).pipe(response);
      }
      return ok(response, { path: result.target.relative, content: result.content, language: result.language, editable: result.editable, image: Boolean(result.mediaType), mediaType: result.mediaType ?? null, size: result.info.size, modifiedAt: result.info.mtime.toISOString(), lines: result.content === undefined ? null : result.content.split(/\r?\n/).length });
    } catch (error) { return fileError(response, error); }
  }));
  app.post("/api/projects/:id/files", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const result = await createProjectEntry(db, own(request), String(request.params.id), { path: text(request.body.path, 1000), type: request.body.type === "folder" ? "folder" : "file", content: typeof request.body.content === "string" ? request.body.content : "" });
      audit(db, request, "file.created", "project", String(request.params.id), { path: result.path, changeId: result.id }); return ok(response, result, 201);
    } catch (error) { return fileError(response, error); }
  }));
  app.patch("/api/projects/:id/files", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const input: { path: string; content?: string; destination?: string } = { path: text(request.body.path, 1000) };
      if (typeof request.body.content === "string") input.content = request.body.content;
      if (typeof request.body.destination === "string") input.destination = text(request.body.destination, 1000);
      const result = await updateProjectEntry(db, own(request), String(request.params.id), input);
      audit(db, request, `file.${result.changeType}`, "project", String(request.params.id), { path: result.path, changeId: result.id }); return ok(response, result);
    } catch (error) { return fileError(response, error); }
  }));
  app.delete("/api/projects/:id/files", requireCsrf, asyncRoute(async (request, response) => {
    try {
      const result = await deleteProjectEntry(db, own(request), String(request.params.id), text(request.query.path, 1000));
      audit(db, request, "file.deleted", "project", String(request.params.id), { path: result.path, changeId: result.id }); return ok(response, result);
    } catch (error) { return fileError(response, error); }
  }));
  app.post("/api/projects/:id/uploads", requireCsrf, projectUpload.single("file"), asyncRoute(async (request, response) => {
    try {
      if (!request.file) throw new FileServiceError("FILE_UPLOAD_REQUIRED", "Choose a file to upload.");
      const result = await uploadProjectAsset(db, own(request), String(request.params.id), text(request.body.path, 1000), request.file);
      audit(db, request, "file.uploaded", "project", String(request.params.id), { path: result.path, changeId: result.id }); return ok(response, result, 201);
    } catch (error) { return fileError(response, error); }
  }));
  app.get("/api/projects/:id", asyncRoute(async (request, response) => {
    try { return ok(response, await projectDetails(db, own(request), String(request.params.id))); }
    catch { return fail(response, 404, "NOT_FOUND", "Project not found."); }
  }));

  app.get("/api/threads", asyncRoute(async (request: AuthenticatedRequest, response) => {
    const projectId = text(request.query.projectId, 64); const search = text(request.query.search, 100); const archived = request.query.archived === "true" ? 1 : 0;
    if (!projectId) return fail(response, 422, "PROJECT_INVALID", "Select a valid project first.");
    try { await getProjectById(db, own(request), projectId); } catch { return fail(response, 422, "PROJECT_INVALID", "Select a valid project first."); }
    const clauses = ["user_id=?", "archived=?", "project_id=?"]; const values: any[] = [own(request), archived, projectId];
    if (search) { clauses.push("title LIKE ? ESCAPE '\\'"); values.push(`%${search.replace(/[%_\\]/g, "\\$&")}%`); }
    const rows = db.prepare(`SELECT * FROM threads WHERE ${clauses.join(" AND ")} ORDER BY last_activity_at DESC LIMIT 100`).all(...values);
    return ok(response, rows.map(serializeThread));
  }));
  app.post("/api/threads", requireCsrf, asyncRoute(async (request: AuthenticatedRequest, response) => {
    const project = db.prepare("SELECT * FROM projects WHERE id=?").get(text(request.body.projectId, 64)) as any;
    if (!project) return fail(response, 422, "PROJECT_INVALID", "Select a valid project first.");
    try { await validateWorkspacePath(project.canonical_path, true); } catch { return fail(response, 422, "PROJECT_INVALID", "The selected project is no longer available."); }
    const id = randomUUID(); const title = text(request.body.title, 120) || "New conversation";
    db.prepare("INSERT INTO threads (id,user_id,project_id,project_path,title) VALUES (?,?,?,?,?)").run(id, own(request), project.id, project.canonical_path, title);
    recordProjectActivity(db, own(request), project.id, "conversation_created", "Created a conversation", { threadId: id });
    return ok(response, serializeThread(db.prepare("SELECT * FROM threads WHERE id=?").get(id)), 201);
  }));
  app.get("/api/threads/:id", (request: AuthenticatedRequest, response) => {
    const row = threadForUser(db, String(request.params.id), own(request)); return row ? ok(response, serializeThread(row)) : fail(response, 404, "NOT_FOUND", "Conversation not found.");
  });
  app.patch("/api/threads/:id", requireCsrf, (request: AuthenticatedRequest, response) => {
    const row = threadForUser(db, String(request.params.id), own(request)); if (!row) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    const title = text(request.body.title, 120); if (!title) return fail(response, 422, "VALIDATION_ERROR", "A conversation title is required.");
    db.prepare("UPDATE threads SET title=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(title, row.id); return ok(response, serializeThread(db.prepare("SELECT * FROM threads WHERE id=?").get(row.id)));
  });
  for (const [route, archived] of [["archive", 1], ["restore", 0]] as const) app.post(`/api/threads/:id/${route}`, requireCsrf, (request: AuthenticatedRequest, response) => {
    const row = threadForUser(db, String(request.params.id), own(request)); if (!row) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    db.prepare("UPDATE threads SET archived=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(archived, row.id); audit(db, request, `thread.${route}`, "thread", row.id); return ok(response, { id: row.id, archived: Boolean(archived) });
  });
  app.delete("/api/threads/:id", requireCsrf, (request: AuthenticatedRequest, response) => {
    const row = threadForUser(db, String(request.params.id), own(request)); if (!row) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    audit(db, request, "thread.deleted", "thread", row.id, { title: row.title }); db.prepare("DELETE FROM threads WHERE id=?").run(row.id); return ok(response, { deleted: true });
  });

  app.get("/api/threads/:id/messages", (request: AuthenticatedRequest, response) => {
    const thread = threadForUser(db, String(request.params.id), own(request)); if (!thread) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    const limit = Math.min(Number(request.query.limit) || 40, 100); const before = text(request.query.before, 50);
    const rows = before
      ? db.prepare("SELECT * FROM messages WHERE thread_id=? AND created_at<? ORDER BY created_at DESC LIMIT ?").all(thread.id, before, limit)
      : db.prepare("SELECT * FROM messages WHERE thread_id=? ORDER BY created_at DESC LIMIT ?").all(thread.id, limit);
    return ok(response, { messages: rows.reverse().map(serializeMessage), hasMore: rows.length === limit });
  });

  const createTurn = async (request: AuthenticatedRequest, response: Response, threadId: string, prompt: string, parentMessageId?: string, attachmentIds: string[] = []) => {
    const thread = threadForUser(db, threadId, own(request)); if (!thread) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    try {
      const project = await getProjectById(db, own(request), thread.project_id);
      if (project.path !== thread.project_path) throw new Error("PROJECT_INVALID");
    } catch { return fail(response, 422, "PROJECT_INVALID", "The conversation project is no longer available."); }
    if (!prompt || prompt.length > 100_000) return fail(response, 422, "VALIDATION_ERROR", "Enter a message no longer than 100,000 characters.");
    const active = db.prepare("SELECT id FROM tasks WHERE project_id=? AND status IN ('queued','started','running')").get(thread.project_id) as any;
    if (active) return fail(response, 409, "CONFLICT", "This project already has an active task.", { taskId: active.id });
    const userMessageId = randomUUID(), assistantMessageId = randomUUID(), taskId = randomUUID();
    const safeAttachmentIds = attachmentIds.filter((id) => typeof id === "string").slice(0, 8);
    for (const id of safeAttachmentIds) {
      const attachment = db.prepare("SELECT id FROM message_attachments WHERE id=? AND user_id=? AND message_id IS NULL").get(id, own(request));
      if (!attachment) return fail(response, 422, "VALIDATION_ERROR", "One or more attachments are unavailable.");
    }
    db.exec("BEGIN IMMEDIATE");
    try {
      db.prepare("INSERT INTO messages (id,thread_id,role,content,metadata_json,parent_message_id) VALUES (?,?,'user',?,?,?)").run(userMessageId, thread.id, prompt, json({ attachmentIds: safeAttachmentIds }), parentMessageId ?? null);
      for (const id of safeAttachmentIds) db.prepare("UPDATE message_attachments SET message_id=?,expires_at=NULL WHERE id=?").run(userMessageId, id);
      db.prepare("INSERT INTO messages (id,thread_id,role,completion_state,task_id,parent_message_id) VALUES (?,?,'assistant','pending',?,?)").run(assistantMessageId, thread.id, taskId, userMessageId);
      db.prepare("INSERT INTO tasks (id,thread_id,project_id,user_message_id,assistant_message_id,provider,status) VALUES (?,?,?,?,?,?,'queued')").run(taskId, thread.id, thread.project_id, userMessageId, assistantMessageId, engine.provider);
      const title = thread.title === "New conversation" ? prompt.replace(/\s+/g, " ").slice(0, 72) : thread.title;
      db.prepare("UPDATE threads SET title=?,updated_at=CURRENT_TIMESTAMP,last_activity_at=CURRENT_TIMESTAMP WHERE id=?").run(title, thread.id);
      db.prepare("DELETE FROM conversation_drafts WHERE user_id=? AND thread_id=?").run(own(request), thread.id);
      db.exec("COMMIT");
    } catch (error) { db.exec("ROLLBACK"); throw error; }
    audit(db, request, "task.started", "task", taskId, { threadId: thread.id });
    recordProjectActivity(db, own(request), thread.project_id, "task_started", "Started a development task", { taskId, threadId: thread.id });
    await engine.start(taskId, thread.project_path, prompt);
    return ok(response, { taskId, userMessage: serializeMessage(db.prepare("SELECT * FROM messages WHERE id=?").get(userMessageId)), assistantMessage: serializeMessage(db.prepare("SELECT * FROM messages WHERE id=?").get(assistantMessageId)) }, 202);
  };

  app.post("/api/threads/:id/messages", requireCsrf, asyncRoute(async (request, response) => createTurn(request, response, String(request.params.id), text(request.body.content, 100_001), undefined, Array.isArray(request.body.attachmentIds) ? request.body.attachmentIds : [])));
  app.patch("/api/messages/:id", requireCsrf, (request: AuthenticatedRequest, response) => {
    const row = db.prepare("SELECT m.* FROM messages m JOIN threads t ON t.id=m.thread_id WHERE m.id=? AND t.user_id=? AND m.role='user'").get(String(request.params.id), own(request)) as any;
    const content = text(request.body.content, 100_001); if (!row || !content || content.length > 100_000) return fail(response, 422, "VALIDATION_ERROR", "This message cannot be edited.");
    db.prepare("UPDATE messages SET content=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(content, row.id); return ok(response, serializeMessage(db.prepare("SELECT * FROM messages WHERE id=?").get(row.id)));
  });
  app.post("/api/messages/:id/retry", requireCsrf, asyncRoute(async (request, response) => {
    const row = db.prepare("SELECT m.*,t.user_id FROM messages m JOIN threads t ON t.id=m.thread_id WHERE m.id=? AND t.user_id=?").get(String(request.params.id), own(request)) as any;
    if (!row) return fail(response, 404, "NOT_FOUND", "Message not found.");
    const user = row.role === "user" ? row : db.prepare("SELECT * FROM messages WHERE id=?").get(row.parent_message_id) as any;
    return createTurn(request, response, row.thread_id, user?.content ?? "Continue", user?.id);
  }));

  app.get("/api/threads/:id/draft", (request: AuthenticatedRequest, response) => {
    if (!threadForUser(db, String(request.params.id), own(request))) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    const row = db.prepare("SELECT content,updated_at FROM conversation_drafts WHERE user_id=? AND thread_id=?").get(own(request), String(request.params.id)) as any;
    return ok(response, row ?? { content: "", updated_at: null });
  });
  app.put("/api/threads/:id/draft", requireCsrf, (request: AuthenticatedRequest, response) => {
    if (!threadForUser(db, String(request.params.id), own(request))) return fail(response, 404, "NOT_FOUND", "Conversation not found.");
    const preferences = db.prepare("SELECT persist_drafts FROM settings WHERE user_id=?").get(own(request)) as any;
    if (!preferences?.persist_drafts) { db.prepare("DELETE FROM conversation_drafts WHERE user_id=? AND thread_id=?").run(own(request), String(request.params.id)); return ok(response, { saved: false }); }
    const content = typeof request.body.content === "string" ? request.body.content.slice(0, 100_000) : "";
    db.prepare(`INSERT INTO conversation_drafts (user_id,thread_id,content,updated_at) VALUES (?,?,?,CURRENT_TIMESTAMP)
      ON CONFLICT(user_id,thread_id) DO UPDATE SET content=excluded.content,updated_at=CURRENT_TIMESTAMP`).run(own(request), String(request.params.id), content);
    return ok(response, { saved: true });
  });

  app.post("/api/tasks", requireCsrf, asyncRoute(async (request, response) => createTurn(request, response, text(request.body.threadId, 64), text(request.body.content, 100_001))));
  app.get("/api/tasks/:id/files", asyncRoute(async (request, response) => {
    try { return ok(response, await taskChangedFiles(db, own(request), String(request.params.id))); }
    catch (error) { return fileError(response, error); }
  }));
  app.get("/api/tasks/:id/diff", asyncRoute(async (request, response) => {
    try {
      const changeId = text(request.query.changeId, 64);
      const version = request.query.version;
      if (changeId && (version === "original" || version === "changed")) return ok(response, { changeId, version, content: await taskDiffVersion(db, own(request), String(request.params.id), changeId, version) });
      const result = await taskDiff(db, own(request), String(request.params.id), changeId || undefined);
      if (request.query.download === "true") { response.set({ "Content-Type": "text/x-diff; charset=utf-8", "Content-Disposition": `attachment; filename="ux7-${String(request.params.id).slice(0, 12)}.patch"`, "Cache-Control": "private, no-store" }); return response.send(result.patch); }
      return ok(response, result);
    } catch (error) { return fileError(response, error); }
  }));
  app.post("/api/tasks/:id/revert", requireCsrf, asyncRoute(async (request, response) => {
    try { const result = await revertTaskChanges(db, own(request), String(request.params.id)); audit(db, request, "task.reverted", "task", String(request.params.id), result); return ok(response, result); }
    catch (error) { return fileError(response, error); }
  }));
  app.post("/api/tasks/:id/checkpoint", requireCsrf, asyncRoute(async (request, response) => {
    try { const result = await checkpointTask(db, own(request), String(request.params.id), text(request.body.message, 120)); audit(db, request, "task.checkpoint", "task", String(request.params.id), result); return ok(response, result); }
    catch (error) { return fileError(response, error); }
  }));
  app.get("/api/tasks/:id", (request: AuthenticatedRequest, response) => { const row = taskForUser(db, String(request.params.id), own(request)); return row ? ok(response, row) : fail(response, 404, "NOT_FOUND", "Task not found."); });
  app.post("/api/tasks/:id/stop", requireCsrf, asyncRoute(async (request, response) => {
    const task = taskForUser(db, String(request.params.id), own(request)); if (!task) return fail(response, 404, "NOT_FOUND", "Task not found.");
    try { await getProjectById(db, own(request), task.project_id); } catch { return fail(response, 422, "PROJECT_INVALID", "The task project is no longer available."); }
    if (!["queued", "started", "running"].includes(task.status)) return fail(response, 409, "CONFLICT", "This task is no longer active.");
    await engine.stop(task.id); audit(db, request, "task.stopped", "task", task.id); return ok(response, { id: task.id, stopping: true });
  }));
  app.post("/api/tasks/:id/retry", requireCsrf, asyncRoute(async (request, response) => {
    const task = taskForUser(db, String(request.params.id), own(request)); if (!task || !["error", "stopped", "completed"].includes(task.status)) return fail(response, 409, "CONFLICT", "This task cannot be retried.");
    const userMessage = db.prepare("SELECT content FROM messages WHERE id=?").get(task.user_message_id) as any;
    return createTurn(request, response, task.thread_id, userMessage.content, task.user_message_id);
  }));
  app.get("/api/tasks/:id/events", (request: AuthenticatedRequest, response) => {
    const task = taskForUser(db, String(request.params.id), own(request)); if (!task) return fail(response, 404, "NOT_FOUND", "Task not found.");
    response.set({ "Content-Type": "text/event-stream", "Cache-Control": "no-store, no-cache, must-revalidate", "X-Accel-Buffering": "no", Connection: "keep-alive" });
    response.flushHeaders();
    let after = Number(request.get("last-event-id") ?? request.query.after ?? 0) || 0;
    let closed = false; request.on("close", () => { closed = true; clearInterval(timer); });
    const send = () => {
      if (closed) return;
      const events = db.prepare("SELECT * FROM task_events WHERE task_id=? AND id>? ORDER BY id LIMIT 100").all(task.id, after) as any[];
      for (const event of events) {
        after = event.id;
        response.write(`id: ${event.id}\nevent: ${event.event_type}\ndata: ${json({ id: event.id, type: event.event_type, label: event.label, detail: event.detail, payload: parseJson(event.payload_json, {}), createdAt: event.created_at })}\n\n`);
      }
      const current = db.prepare("SELECT status FROM tasks WHERE id=?").get(task.id) as any;
      if (["completed", "stopped", "error"].includes(current.status) && events.length === 0) { clearInterval(timer); response.end(); }
    };
    send(); const timer = setInterval(send, 250); const heartbeat = setInterval(() => { if (!closed) response.write(`event: heartbeat\ndata: {"time":"${now()}"}\n\n`); }, 15_000);
    response.on("close", () => clearInterval(heartbeat));
  });

  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: config.uploadMaxBytes, files: 1 } });
  app.post("/api/uploads", requireCsrf, upload.single("file"), asyncRoute(async (request, response) => {
    if (!request.file) return fail(response, 422, "UPLOAD_REJECTED", "Choose a supported file.");
    try {
      const safe = validateUpload(request.file); await mkdir(config.uploadRoot, { recursive: true });
      const id = randomUUID(), storageName = `${randomUUID()}.upload`; await writeFile(path.join(config.uploadRoot, storageName), request.file.buffer, { mode: 0o600, flag: "wx" });
      const expiresAt = new Date(Date.now() + config.uploadRetentionHours * 3_600_000).toISOString();
      db.prepare("INSERT INTO message_attachments (id,user_id,storage_name,original_name,media_type,size_bytes,expires_at) VALUES (?,?,?,?,?,?,?)").run(id, own(request), storageName, safe.name, safe.mediaType, request.file.size, expiresAt);
      return ok(response, { id, name: safe.name, mediaType: safe.mediaType, size: request.file.size }, 201);
    } catch { return fail(response, 422, "UPLOAD_REJECTED", "This file type, name, or size is not allowed."); }
  }));
  app.get("/api/uploads/:id", asyncRoute(async (request, response) => {
    const row = db.prepare("SELECT * FROM message_attachments WHERE id=? AND user_id=?").get(String(request.params.id), own(request)) as any;
    if (!row) return fail(response, 404, "NOT_FOUND", "Attachment not found.");
    const filePath = path.join(config.uploadRoot, row.storage_name); await stat(filePath);
    response.set({ "Content-Type": row.media_type, "Content-Length": String(row.size_bytes), "Content-Disposition": `${row.media_type.startsWith("image/") ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(row.original_name)}`, "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" });
    return createReadStream(filePath).pipe(response);
  }));
  app.delete("/api/uploads/:id", requireCsrf, asyncRoute(async (request, response) => {
    const row = db.prepare("SELECT * FROM message_attachments WHERE id=? AND user_id=?").get(String(request.params.id), own(request)) as any;
    if (!row) return fail(response, 404, "NOT_FOUND", "Attachment not found.");
    await rm(path.join(config.uploadRoot, row.storage_name), { force: true }); db.prepare("DELETE FROM message_attachments WHERE id=?").run(row.id); return ok(response, { deleted: true });
  }));

  app.get("/api/settings", (request: AuthenticatedRequest, response) => {
    const settings = db.prepare("SELECT * FROM settings WHERE user_id=?").get(own(request)); return ok(response, { ...settings, sessionTimeoutSeconds: config.sessionTtlSeconds, agent: engine.provider === "mock" ? "Development adapter" : "Configured adapter" });
  });
  app.patch("/api/settings", requireCsrf, (request: AuthenticatedRequest, response) => {
    const defaultProjectId = typeof request.body.defaultProjectId === "string" && db.prepare("SELECT id FROM projects WHERE id=?").get(request.body.defaultProjectId) ? request.body.defaultProjectId : undefined;
    const allowed = { theme: ["dark", "light", "system"].includes(request.body.theme) ? request.body.theme : undefined, default_project_id: defaultProjectId, conversation_retention_days: Number.isInteger(request.body.conversationRetentionDays) ? Math.max(1, Math.min(3650, request.body.conversationRetentionDays)) : undefined, persist_drafts: typeof request.body.persistDrafts === "boolean" ? Number(request.body.persistDrafts) : undefined, compact_activity: typeof request.body.compactActivity === "boolean" ? Number(request.body.compactActivity) : undefined, auto_scroll: typeof request.body.autoScroll === "boolean" ? Number(request.body.autoScroll) : undefined, wrap_code: typeof request.body.wrapCode === "boolean" ? Number(request.body.wrapCode) : undefined, notifications: typeof request.body.notifications === "boolean" ? Number(request.body.notifications) : undefined };
    const entries = Object.entries(allowed).filter(([, value]) => value !== undefined); if (!entries.length) return fail(response, 422, "VALIDATION_ERROR", "No supported settings were supplied.");
    db.prepare(`UPDATE settings SET ${entries.map(([key]) => `${key}=?`).join(",")},updated_at=CURRENT_TIMESTAMP WHERE user_id=?`).run(...entries.map(([, value]) => value), own(request)); audit(db, request, "settings.changed", "user", own(request), Object.fromEntries(entries));
    return ok(response, db.prepare("SELECT * FROM settings WHERE user_id=?").get(own(request)));
  });

  app.use("/api", (_request, response) => fail(response, 404, "NOT_FOUND", "API route not found."));
  app.use(express.static(config.publicRoot, { etag: true, index: false, setHeaders: (response, filePath) => {
    if (filePath.endsWith("sw.js") || filePath.endsWith("manifest.webmanifest") || filePath.endsWith("offline.html")) response.setHeader("Cache-Control", "no-cache");
    else response.setHeader("Cache-Control", "public, max-age=86400");
  } }));
  app.get("*splat", (_request, response) => response.sendFile(path.join(config.publicRoot, "index.html")));
  app.use((error: any, _request: Request, response: Response, _next: NextFunction) => {
    if (error?.code === "LIMIT_FILE_SIZE") return fail(response, 413, "UPLOAD_REJECTED", "The attachment exceeds the configured size limit.");
    if (config.env === "development") console.error(error);
    return fail(response, 500, "INTERNAL_ERROR", "UX7 could not complete the request.");
  });

  return { app, db, engine };
}

export async function cleanupExpiredUploads(db: Db) {
  const rows = db.prepare("SELECT id,storage_name FROM message_attachments WHERE expires_at<CURRENT_TIMESTAMP").all() as any[];
  for (const row of rows) { await rm(path.join(config.uploadRoot, row.storage_name), { force: true }); db.prepare("DELETE FROM message_attachments WHERE id=?").run(row.id); }
  const known = new Set((db.prepare("SELECT storage_name FROM message_attachments").all() as any[]).map((row) => row.storage_name));
  try { for (const file of await readdir(config.uploadRoot)) if (!known.has(file) && file.endsWith(".upload")) await rm(path.join(config.uploadRoot, file), { force: true }); } catch { /* Upload directory is created lazily. */ }
  return rows.length;
}

export function withStartupTimeout<T>(operation: PromiseLike<T>, label: string, timeoutMs = config.startupTimeoutMs): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
    Promise.resolve(operation).then(
      (value) => { clearTimeout(timer); resolve(value); },
      (error) => { clearTimeout(timer); reject(error); },
    );
  });
}

interface StartServerOptions {
  databasePath?: string;
  host?: string;
  port?: number;
  initializationTimeoutMs?: number;
}

export async function startServer(options: StartServerOptions = {}): Promise<Awaited<ReturnType<typeof createApp>> & { server: Server }> {
  const host = options.host ?? config.host;
  const port = options.port ?? config.port;
  const initializationTimeoutMs = options.initializationTimeoutMs ?? config.startupTimeoutMs;

  console.log(`[UX7 startup] Starting process ${process.pid}.`);
  console.log(`[UX7 startup] Initializing database and applying migrations (timeout: ${initializationTimeoutMs}ms).`);
  const instance = await withStartupTimeout(
    createApp(options.databasePath === undefined ? {} : { databasePath: options.databasePath }),
    "Database and application initialization",
    initializationTimeoutMs,
  );

  console.log(`[UX7 startup] Initialization complete; calling listen() on ${host}:${port}.`);
  const server = await new Promise<Server>((resolve, reject) => {
    const listeningServer = instance.app.listen(port, host);
    const onError = (error: Error) => reject(error);
    listeningServer.once("error", onError);
    listeningServer.once("listening", () => {
      listeningServer.off("error", onError);
      console.log(`[UX7 startup] HTTP server is listening on ${host}:${port}.`);
      resolve(listeningServer);
    });
  }).catch((error) => {
    instance.db.close();
    throw error;
  });

  // Project discovery is request-driven, and upload cleanup remains an external
  // maintenance job, so neither optional operation can delay Passenger readiness.
  return { ...instance, server };
}

const entrypoint = process.argv[1] ? path.resolve(process.argv[1]) : "";
const isDirectEntrypoint = entrypoint.endsWith(path.join("src", "server.ts")) || entrypoint.endsWith(path.join("dist", "server.js"));
if (isDirectEntrypoint) {
  await startServer().catch((error) => {
    console.error("[UX7 startup] Fatal startup failure:", error);
    process.exitCode = 1;
  });
}
