import express from "express";
import { readFileSync } from "node:fs";
import { gunzipSync, gzipSync } from "node:zlib";

import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";
import { GitStorageError, TodoGitStorage, gitStorageConfigFromEnv, snapshotHash } from "./git-storage";
import { loadReminderNotifierConfig, TodoReminderNotifier } from "./reminder-notifier";
import { PostgresTodoRepository, normalizeTodoSnapshot, type TodoReminderNotification, type TodoSnapshot } from "./repository";
import type { TodoAction } from "./types";

type LogLevel = "info" | "warn" | "error";

class HttpError extends Error {
  constructor(readonly status: number, message: string, readonly detail: Record<string, unknown> = {}) {
    super(message);
    this.name = "HttpError";
  }
}

function envString(name: string, fallback: string): string {
  const value = process.env[name];
  return value === undefined || value.length === 0 ? fallback : value;
}

function envPort(name: string, fallback: number): number {
  const value = Number(process.env[name] || fallback);
  if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`${name} must be a valid TCP port`);
  return value;
}

function errorDetail(error: unknown): Record<string, unknown> {
  if (error instanceof GitStorageError) return { name: error.name, message: error.message, ...error.detail };
  if (error instanceof HttpError) return { name: error.name, message: error.message, ...error.detail };
  if (error instanceof Error) return { name: error.name, message: error.message };
  return { name: "Error", message: String(error) };
}

const host = envString("HOST", "0.0.0.0");
const port = envPort("PORT", 4288);
const isCheckMode = process.execArgv.includes("--check");
const databaseUrl = envString("DATABASE_URL", isCheckMode ? "postgres://syntax-check@127.0.0.1:1/syntax-check" : "");
if (!databaseUrl) throw new Error("DATABASE_URL is required");
const logFile = envString("LOG_FILE", "/var/log/unidesk/todo-note.jsonl");
const repository = new PostgresTodoRepository(databaseUrl);
const gitStorage = new TodoGitStorage(gitStorageConfigFromEnv());
const startedAt = new Date().toISOString();
const recentLogs: Record<string, unknown>[] = [];
const logWriter = createHourlyJsonlWriter({
  baseLogFile: logFile,
  service: "todo-note",
  maxBytes: logRetentionBytesForService("todo-note"),
});
let storageState: Record<string, unknown> = { ok: false, initialized: false };
let mutationQueue: Promise<unknown> = Promise.resolve();
let garbageCollectionScheduled = false;

function cgroupMemoryBytes(path: string): number | null {
  try {
    const raw = readFileSync(path, "utf8").trim();
    if (raw === "max") return null;
    const value = Number(raw);
    return Number.isFinite(value) && value >= 0 ? value : null;
  } catch {
    return null;
  }
}

function memorySummary(): Record<string, unknown> {
  const usage = process.memoryUsage();
  return {
    rssBytes: usage.rss,
    heapUsedBytes: usage.heapUsed,
    heapTotalBytes: usage.heapTotal,
    externalBytes: usage.external,
    arrayBuffersBytes: usage.arrayBuffers,
    cgroup: {
      currentBytes: cgroupMemoryBytes("/sys/fs/cgroup/memory.current"),
      peakBytes: cgroupMemoryBytes("/sys/fs/cgroup/memory.peak"),
      limitBytes: cgroupMemoryBytes("/sys/fs/cgroup/memory.max"),
    },
  };
}

function scheduleGarbageCollection(): void {
  if (garbageCollectionScheduled) return;
  garbageCollectionScheduled = true;
  setTimeout(() => {
    garbageCollectionScheduled = false;
    Bun.gc(true);
  }, 0);
}

async function captureRollbackSnapshot(): Promise<Buffer> {
  const snapshot = await repository.exportSnapshot();
  return gzipSync(JSON.stringify(snapshot), { level: 1 });
}

function restoreRollbackSnapshot(compressed: Buffer): TodoSnapshot {
  return normalizeTodoSnapshot(JSON.parse(gunzipSync(compressed).toString("utf8")) as unknown);
}

function log(level: LogLevel, event: string, detail: Record<string, unknown> = {}): void {
  const record = { at: new Date().toISOString(), service: "todo-note", level, event, ...detail };
  recentLogs.push(record);
  while (recentLogs.length > 300) recentLogs.shift();
  process.stdout.write(`${JSON.stringify(record)}\n`);
  logWriter.appendJson(record);
}

function deployInfo(): Record<string, unknown> {
  return {
    serviceId: envString("UNIDESK_DEPLOY_SERVICE_ID", "todo-note"),
    repo: envString("UNIDESK_DEPLOY_REPO", ""),
    commit: envString("UNIDESK_DEPLOY_COMMIT", ""),
    requestedCommit: envString("UNIDESK_DEPLOY_REQUESTED_COMMIT", ""),
  };
}

async function refreshStorageState(): Promise<Record<string, unknown>> {
  const snapshot = await repository.exportSnapshot();
  const verification = await gitStorage.verify(snapshot);
  storageState = {
    ok: verification.ok === true,
    initialized: true,
    checkedAt: new Date().toISOString(),
    config: gitStorage.summary(),
    verification,
  };
  return storageState;
}

async function initializeStorage(): Promise<void> {
  await repository.ensureReady();
  if (!gitStorage.enabled()) {
    storageState = {
      ok: true,
      initialized: true,
      checkedAt: new Date().toISOString(),
      config: gitStorage.summary(),
    };
    return;
  }
  const databaseReminderNotifications = await repository.listReminderNotifications();
  const gitSnapshot = await gitStorage.readSnapshot();
  if (gitSnapshot === null) {
    const databaseSnapshot = await repository.exportSnapshot();
    const result = await gitStorage.writeSnapshot(databaseSnapshot, "initialize storage");
    recordSuccessfulWrite(databaseSnapshot, result);
    log("info", "github_storage_initialized", { instances: databaseSnapshot.instances.length, result });
  } else {
    const mergedSnapshot = {
      ...gitSnapshot,
      generatedAt: new Date().toISOString(),
      reminderNotifications: mergeReminderNotifications(gitSnapshot.reminderNotifications, databaseReminderNotifications),
    };
    const hydratedSnapshot = await repository.replaceSnapshot(mergedSnapshot, {}, { trustedSnapshot: true });
    const mergedHash = snapshotHash(mergedSnapshot);
    const persisted = mergedHash === snapshotHash(gitSnapshot)
      ? { ok: true, changed: false, primary: "github-repo", hash: mergedHash, head: gitStorage.summary().head }
      : await gitStorage.writeSnapshot(mergedSnapshot, "merge reminder notifications");
    recordSuccessfulWrite(hydratedSnapshot, persisted);
    log("info", "github_storage_hydrated", {
      instances: mergedSnapshot.instances.length,
      reminderNotifications: mergedSnapshot.reminderNotifications.length,
      hash: mergedHash,
      memory: memorySummary(),
    });
  }
  scheduleGarbageCollection();
}

function mergeReminderNotifications(gitValues: TodoReminderNotification[], databaseValues: TodoReminderNotification[]): TodoReminderNotification[] {
  const merged = new Map<string, TodoReminderNotification>();
  for (const value of [...gitValues, ...databaseValues]) merged.set(`${value.instanceId}\0${value.todoId}\0${value.reminderAt}`, value);
  return [...merged.values()].sort((left, right) => left.sentAt.localeCompare(right.sentAt));
}

async function enqueueMutation<T>(operation: () => Promise<T>): Promise<T> {
  const next = mutationQueue.then(operation, operation);
  mutationQueue = next.then(() => undefined, () => undefined);
  return await next;
}

function recordSuccessfulWrite(snapshot: TodoSnapshot, persisted: Record<string, unknown>): void {
  const hash = typeof persisted.hash === "string" ? persisted.hash : snapshotHash(snapshot);
  const config = gitStorage.summary();
  storageState = {
    ok: true,
    initialized: true,
    checkedAt: new Date().toISOString(),
    config,
    verification: {
      ok: true,
      primary: gitStorage.enabled() ? "github-repo" : "postgres",
      instances: snapshot.instances.length,
      reminderNotifications: snapshot.reminderNotifications.length,
      databaseHash: hash,
      gitHash: gitStorage.enabled() ? hash : null,
      head: config.head,
    },
    lastWrite: persisted,
  };
}

async function mutate<T>(reason: string, operation: () => Promise<T>): Promise<T> {
  try {
    return await enqueueMutation(async () => {
      if (!gitStorage.enabled()) return await operation();
      const rollbackSnapshot = await captureRollbackSnapshot();
      Bun.gc(true);
      const result = await operation();
      try {
        const after = await repository.exportSnapshot();
        const persisted = await gitStorage.writeSnapshot(after, reason);
        recordSuccessfulWrite(after, persisted);
        log("info", "github_storage_write_complete", {
          reason,
          instances: after.instances.length,
          historyEntries: after.instances.reduce((total, entry) => total + entry.history.length, 0),
          memory: memorySummary(),
        });
        return result;
      } catch (error) {
        try {
          await repository.replaceSnapshot(restoreRollbackSnapshot(rollbackSnapshot), {}, {
            preserveReminderNotifications: true,
            trustedSnapshot: true,
          });
        } catch (rollbackError) {
          log("error", "database_rollback_failed", { reason, error: errorDetail(rollbackError) });
        }
        storageState = {
          ok: false,
          initialized: true,
          checkedAt: new Date().toISOString(),
          config: gitStorage.summary(),
          error: errorDetail(error),
        };
        throw error;
      }
    });
  } finally {
    scheduleGarbageCollection();
  }
}

async function replaceFromImport(snapshotValue: unknown): Promise<Record<string, unknown>> {
  const snapshot = normalizeTodoSnapshot(snapshotValue);
  try {
    return await enqueueMutation(async () => {
      const rollbackSnapshot = await captureRollbackSnapshot();
      Bun.gc(true);
      let imported: TodoSnapshot;
      let persisted: Record<string, unknown>;
      try {
        imported = await repository.replaceSnapshot(snapshot, {}, { trustedSnapshot: true });
        persisted = await gitStorage.writeSnapshot(imported, "migrate from CC01");
      } catch (error) {
        try {
          await repository.replaceSnapshot(restoreRollbackSnapshot(rollbackSnapshot), {}, { trustedSnapshot: true });
        } catch (rollbackError) {
          log("error", "database_rollback_failed", { reason: "migrate from CC01", error: errorDetail(rollbackError) });
        }
        throw error;
      }
      recordSuccessfulWrite(imported, persisted);
      return {
        ok: true,
        instances: imported.instances.length,
        historyEntries: imported.instances.reduce((total, entry) => total + entry.history.length, 0),
        reminderNotifications: imported.reminderNotifications.length,
        hash: typeof persisted.hash === "string" ? persisted.hash : snapshotHash(imported),
        persisted,
      };
    });
  } finally {
    scheduleGarbageCollection();
  }
}

function importSnapshotValue(body: unknown): unknown {
  if (typeof body !== "object" || body === null || Array.isArray(body)) throw new HttpError(400, "storage import body must be an object");
  const record = body as Record<string, unknown>;
  const hasSnapshot = record.snapshot !== undefined;
  const hasCompressed = record.snapshotGzipBase64 !== undefined;
  if (hasSnapshot === hasCompressed) throw new HttpError(400, "storage import requires exactly one of snapshot or snapshotGzipBase64");
  if (hasSnapshot) return record.snapshot;
  if (typeof record.snapshotGzipBase64 !== "string" || record.snapshotGzipBase64.length === 0) {
    throw new HttpError(400, "snapshotGzipBase64 must be a non-empty string");
  }
  if (record.snapshotGzipBase64.length > 2 * 1024 * 1024) throw new HttpError(413, "compressed storage import is too large");
  try {
    const compressed = Buffer.from(record.snapshotGzipBase64, "base64");
    if (compressed.length > 1536 * 1024) throw new HttpError(413, "compressed storage import is too large");
    const decoded = gunzipSync(compressed, { maxOutputLength: 20 * 1024 * 1024 });
    return JSON.parse(decoded.toString("utf8")) as unknown;
  } catch (error) {
    if (error instanceof HttpError) throw error;
    throw new HttpError(400, "snapshotGzipBase64 must contain valid gzip-compressed JSON");
  }
}

function healthPayload(): Record<string, unknown> {
  const ok = storageState.ok === true;
  return {
    ok,
    service: "todo-note",
    status: ok ? "ready" : "not-ready",
    startedAt,
    storage: storageState,
    deploy: deployInfo(),
    memory: memorySummary(),
  };
}

async function main(): Promise<void> {
  await initializeStorage();
  const reminderNotifier = new TodoReminderNotifier(repository, { log }, loadReminderNotifierConfig(), databaseUrl, async (writeNotification) => {
    await enqueueMutation(async () => {
      await writeNotification();
      const snapshot = await repository.exportSnapshot();
      try {
        const persisted = await gitStorage.writeSnapshot(snapshot, "record reminder notification");
        recordSuccessfulWrite(snapshot, persisted);
      } catch (error) {
        storageState = { ok: false, initialized: true, checkedAt: new Date().toISOString(), config: gitStorage.summary(), error: errorDetail(error) };
        throw error;
      }
    });
  });
  reminderNotifier.start();
  logWriter.prune();
  const app = express();
  app.use(express.json({ limit: "20mb" }));
  app.use((request, _response, next) => {
    log("info", "http_request", { method: request.method, path: request.path });
    next();
  });

  app.get("/live", (_request, response) => response.json({ ok: true, service: "todo-note", startedAt }));
  app.get(["/health", "/api/health"], async (_request, response, next) => {
    try {
      await repository.ping();
      response.status(storageState.ok === true ? 200 : 503).json({ ...healthPayload(), reminders: reminderNotifier.status() });
    } catch (error) {
      next(error);
    }
  });
  app.get(["/logs", "/api/logs"], (_request, response) => {
    response.type("application/x-ndjson").send(`${recentLogs.map((entry) => JSON.stringify(entry)).join("\n")}\n`);
  });
  app.get("/api/reminders/status", (_request, response) => response.json({ ok: true, reminders: reminderNotifier.status() }));
  app.post("/api/reminders/scan", async (_request, response, next) => {
    try {
      response.json({ ok: true, reminders: await reminderNotifier.scanNow() });
    } catch (error) {
      next(error);
    }
  });
  app.get("/api/storage/status", (_request, response) => response.json(storageState));
  app.get("/api/storage/verify", async (_request, response, next) => {
    try {
      const state = await enqueueMutation(refreshStorageState);
      response.status(state.ok === true ? 200 : 409).json(state);
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/storage/migrate", async (request, response, next) => {
    try {
      const result = await enqueueMutation(async () => {
        const snapshot = await repository.exportSnapshot();
        if (request.body?.dryRun === true) return {
          ok: true,
          dryRun: true,
          instances: snapshot.instances.length,
          historyEntries: snapshot.instances.reduce((total, entry) => total + entry.history.length, 0),
          reminderNotifications: snapshot.reminderNotifications.length,
          hash: snapshotHash(snapshot),
        };
        if (request.body?.confirm !== true) throw new HttpError(400, "storage migration requires confirm=true");
        const persisted = await gitStorage.writeSnapshot(snapshot, "migrate runtime index");
        recordSuccessfulWrite(snapshot, persisted);
        return { ok: true, dryRun: false, result: persisted };
      });
      response.json(result);
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/storage/import", async (request, response, next) => {
    try {
      if (request.body?.confirm !== true) throw new HttpError(400, "storage import requires confirm=true");
      response.json(await replaceFromImport(importSnapshotValue(request.body)));
    } catch (error) {
      next(error);
    }
  });

  app.get("/api/instances", async (_request, response, next) => {
    try {
      response.json(await repository.listInstances());
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/instances", async (request, response, next) => {
    try {
      const name = String(request.body?.name ?? "").trim() || "New List";
      response.status(201).json(await mutate(`create ${name}`, () => repository.createInstance(name)));
    } catch (error) {
      next(error);
    }
  });
  app.get("/api/instances/:instanceId", async (request, response, next) => {
    try {
      response.json(await repository.getInstance(request.params.instanceId));
    } catch (error) {
      next(error);
    }
  });
  app.delete("/api/instances/:instanceId", async (request, response, next) => {
    try {
      await mutate(`delete ${request.params.instanceId}`, () => repository.deleteInstance(request.params.instanceId));
      response.json({ ok: true });
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/instances/:instanceId/actions", async (request, response, next) => {
    try {
      const action = request.body?.action as TodoAction;
      if (!action || typeof action.type !== "string") throw new HttpError(400, "action.type is required");
      response.json(await mutate(`${action.type} ${request.params.instanceId}`, () => repository.applyAction(request.params.instanceId, action)));
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/instances/:instanceId/undo", async (request, response, next) => {
    try {
      response.json(await mutate(`undo ${request.params.instanceId}`, () => repository.undo(request.params.instanceId)));
    } catch (error) {
      next(error);
    }
  });
  app.post("/api/instances/:instanceId/redo", async (request, response, next) => {
    try {
      response.json(await mutate(`redo ${request.params.instanceId}`, () => repository.redo(request.params.instanceId)));
    } catch (error) {
      next(error);
    }
  });

  app.use((_request, response) => response.status(404).json({ ok: false, error: "route not found" }));
  app.use((error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => {
    const detail = errorDetail(error);
    const message = String(detail.message ?? "request failed");
    const status = error instanceof HttpError ? error.status : error instanceof GitStorageError ? 503 : /not found/iu.test(message) ? 404 : 500;
    log("error", "request_failed", { status, error: detail });
    response.status(status).json({ ok: false, error: message, detail });
  });

  const server = app.listen(port, host, () => {
    log("info", "server_started", { host, port, storage: gitStorage.summary() });
  });
  const shutdown = async (signal: string): Promise<void> => {
    log("info", "server_stopping", { signal });
    server.close();
    await reminderNotifier.stop();
    await repository.close();
    process.exit(0);
  };
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
  process.on("SIGINT", () => void shutdown("SIGINT"));
}

if (import.meta.main && !isCheckMode) {
  main().catch((error) => {
    log("error", "server_start_failed", { error: errorDetail(error) });
    process.exitCode = 1;
  });
}
