/**
 * /database/api/* 文件内容与导出端点(契约 9.3/9.4)。
 * 冲突流:POST commits 返回 200 {version} 或 409 {currentVersion}(编辑 UI 拉 diff 后重提)。
 */

import type { FastifyInstance, FastifyRequest } from "fastify";
import {
  commitFile,
  deleteFile,
  diffFile,
  fileHistory,
  listFiles,
  readFile,
  readFileRaw,
  type FileContentEncoding,
} from "../filelib/fileService.js";
import {
  createManifestStubAdapter,
  downloadExport,
  getExportJob,
  submitExport,
} from "../filelib/exportService.js";
import { FileLibError } from "../filelib/model.js";
import {
  actorOrNull,
  bodyObject,
  optionalString,
  requireString,
  sendRouteError,
  type FileLibRouteDeps,
} from "../filelib/routeShared.js";

export async function registerFileRoutes(
  app: FastifyInstance,
  deps: FileLibRouteDeps,
): Promise<void> {
  const fileDeps = {
    prisma: deps.prisma,
    organizationId: deps.organizationId,
    groupResolver: deps.groupResolver,
    versionStore: deps.versionStore,
  };
  const exportDeps = { ...fileDeps, adapters: deps.exportAdapters };

  function pathParam(request: FastifyRequest): string {
    const path = (request.query as { path?: string }).path;
    if (path === undefined) throw new FileLibError(400, "invalid_request", "missing query: path");
    return path;
  }

  function encodingParam(raw: unknown): FileContentEncoding {
    if (raw === undefined || raw === "utf8") return "utf8";
    if (raw === "base64") return "base64";
    throw new FileLibError(400, "invalid_request", "encoding must be utf8 or base64");
  }

  app.get("/database/api/projects/:id/files", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const prefix = (request.query as { prefix?: string }).prefix;
      return { files: await listFiles(fileDeps, actor, id, prefix) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/projects/:id/file", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      return await readFile(fileDeps, actor, id, pathParam(request));
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  // 原始字节下载(Content-Disposition: attachment;浏览器直接 save-as)
  app.get("/database/api/projects/:id/file/raw", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const raw = await readFileRaw(fileDeps, actor, id, pathParam(request));
      return reply
        .header("Content-Disposition",
          `attachment; filename="${encodeURIComponent(raw.filename)}"; filename*=UTF-8''${encodeURIComponent(raw.filename)}`)
        .header("X-Content-Version", raw.version)
        .type("application/octet-stream")
        .send(raw.buffer);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  // 新建/上传(baseVersion 恒 null;已存在 → 409)
  app.put("/database/api/projects/:id/file", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const result = await commitFile(fileDeps, actor, id, {
        path: requireString(body, "path"),
        baseVersion: null,
        content: requireString(body, "content"),
        encoding: encodingParam(body["encoding"]),
        message: optionalString(body, "message"),
      });
      return reply.status(201).send(result);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  // 提交编辑(乐观并发;409 → details.currentVersion + file.conflict_detected 审计)
  app.post("/database/api/projects/:id/file/commits", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const baseVersion = body["baseVersion"];
      if (typeof baseVersion !== "string" || baseVersion === "") {
        throw new FileLibError(400, "invalid_request", "baseVersion must be a non-empty string");
      }
      return await commitFile(fileDeps, actor, id, {
        path: requireString(body, "path"),
        baseVersion,
        content: requireString(body, "content"),
        encoding: encodingParam(body["encoding"]),
        message: optionalString(body, "message"),
      });
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.delete("/database/api/projects/:id/file", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      await deleteFile(fileDeps, actor, id, pathParam(request), requireString(body, "baseVersion"));
      return reply.status(204).send();
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/projects/:id/file/diff", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const query = request.query as { from?: string; to?: string };
      if (query.from === undefined || query.to === undefined) {
        throw new FileLibError(400, "invalid_request", "missing query: from / to");
      }
      return await diffFile(fileDeps, actor, id, pathParam(request), query.from, query.to);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/projects/:id/file/history", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const rawLimit = (request.query as { limit?: string }).limit;
      const limit = rawLimit === undefined ? undefined : Number.parseInt(rawLimit, 10);
      if (limit !== undefined && (!Number.isSafeInteger(limit) || limit <= 0)) {
        throw new FileLibError(400, "invalid_request", "limit must be a positive integer");
      }
      return { history: await fileHistory(fileDeps, actor, id, pathParam(request), limit) };
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  /* ------------------------------------------------------------ 导出(D10 异步) */

  app.post("/database/api/projects/:id/exports", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { id } = request.params as { id: string };
      const body = bodyObject(request.body);
      const params = body["params"];
      if (params !== undefined && (typeof params !== "object" || params === null || Array.isArray(params))) {
        throw new FileLibError(400, "invalid_request", "params must be an object");
      }
      const job = await submitExport(
        exportDeps,
        actor,
        id,
        requireString(body, "target"),
        (params as Record<string, unknown> | undefined) ?? {},
      );
      return reply.status(202).send({ jobId: job.id, status: job.status });
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/exports/:jobId", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { jobId } = request.params as { jobId: string };
      return await getExportJob(exportDeps, actor, jobId);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });

  app.get("/database/api/exports/:jobId/download", async (request, reply) => {
    const actor = await actorOrNull(request, reply, deps);
    if (actor === null) return reply;
    try {
      const { jobId } = request.params as { jobId: string };
      const artifact = await downloadExport(exportDeps, actor, jobId);
      return reply
        .header("Content-Disposition", `attachment; filename="${artifact.filename}"`)
        .type("application/octet-stream")
        .send(artifact.content);
    } catch (error) {
      return sendRouteError(reply, error);
    }
  });
}
