/**
 * 文件内容服务(Phase 3):路径安全 + 版本化文件操作 + 审计。
 *
 * 顺序铁律(Metis 风险#1 的落地):
 *  - 内容写:先 versionStore.commit(业务事实本体)→ 再 DB 事务(审计)。
 *    宁多一个无审计的版本,不造一条假审计。
 *  - conflict:写 file.conflict_detected(冲突本身就是事件),再抛 409。
 *  - 读:随取随读,不写审计(需求 5.2 未列读操作)。
 */

import { FileLibError } from "./model.js";
import { FILE_LIB_AUDIT_ACTIONS, writeFileLibAudit } from "./audit.js";
import type { CommitResult, FileEntry, VersionInfo, VersionStore } from "./versionStore.js";
import type { AccessDeps, FileLibActor } from "./treeService.js";
import { requireAccessInTx } from "./treeService.js";
import type { FileLibNode, PrismaClient } from "@prisma/client";

export const FILE_PATH_MAX_LENGTH = 512;
export const FILE_PATH_MAX_DEPTH = 32;
export const FILE_CONTENT_MAX_BYTES = 10 * 1024 * 1024; // OPEN-5 初值

const CONTROL_CHARS = /[\p{C}]/u;
const FORBIDDEN_SEGMENTS = new Set(["", ".", "..", ".git"]);

/**
 * 路径安全(Metis 安全红线):NFC;拒绝反斜杠、控制字符、空段、
 * "." / ".." / ".git" 段、绝对路径、超长/超深。返回规范化相对路径。
 */
export function validateFilePath(raw: string): string {
  const normalized = raw.normalize("NFC");
  if (normalized.length === 0 || normalized.length > FILE_PATH_MAX_LENGTH) {
    throw new FileLibError(400, "invalid_path", `path must be 1..${FILE_PATH_MAX_LENGTH} characters`);
  }
  if (normalized.includes("\\")) {
    throw new FileLibError(400, "invalid_path", "path must use '/' separators");
  }
  if (CONTROL_CHARS.test(normalized)) {
    throw new FileLibError(400, "invalid_path", "path must not contain control characters");
  }
  const segments = normalized.split("/");
  if (segments.length > FILE_PATH_MAX_DEPTH) {
    throw new FileLibError(400, "invalid_path", `path depth exceeds ${FILE_PATH_MAX_DEPTH}`);
  }
  for (const segment of segments) {
    if (FORBIDDEN_SEGMENTS.has(segment)) {
      throw new FileLibError(400, "invalid_path", `forbidden path segment: "${segment}"`);
    }
  }
  return normalized;
}

export interface FileDeps extends AccessDeps {
  readonly prisma: PrismaClient;
  readonly versionStore: VersionStore;
}

type ProjectChain = { readonly node: FileLibNode; readonly storageDir: string };

async function requireProject(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  minRole: "VIEW" | "EDIT",
): Promise<ProjectChain> {
  const { node } = await deps.prisma.$transaction(async (tx) =>
    requireAccessInTx(tx, deps, actor, projectId, minRole),
  );
  if (node.kind !== "PROJECT") {
    throw new FileLibError(400, "invalid_node_kind", "file operations apply to projects only");
  }
  if (node.provisionStatus === "PROVISIONING") {
    throw new FileLibError(409, "project_not_ready", "project repository is still provisioning");
  }
  if (node.provisionStatus === "FAILED") {
    throw new FileLibError(409, "project_not_ready", "project repository provisioning failed");
  }
  if (node.storageDir === null) {
    throw new FileLibError(500, "storage_missing", "project has no storage directory");
  }
  return { node, storageDir: node.storageDir };
}

export type FileContentEncoding = "utf8" | "base64";

export interface FileContentDto {
  readonly path: string;
  readonly version: string;
  readonly encoding: FileContentEncoding;
  readonly content: string;
  readonly size: number;
}

export function decodeContent(content: string, encoding: FileContentEncoding): string | Buffer {
  return encoding === "base64" ? Buffer.from(content, "base64") : content;
}

function encodeContent(buffer: Buffer): { readonly encoding: FileContentEncoding; readonly content: string } {
  // 粗判二进制:含 NUL 字节即按 base64 返回(需求 2.5 在线编辑仅针对文本)。
  return buffer.includes(0)
    ? { encoding: "base64", content: buffer.toString("base64") }
    : { encoding: "utf8", content: buffer.toString("utf8") };
}

function checkSize(content: string | Buffer): void {
  const bytes = typeof content === "string" ? Buffer.byteLength(content, "utf8") : content.byteLength;
  if (bytes > FILE_CONTENT_MAX_BYTES) {
    throw new FileLibError(413, "file_too_large", `file exceeds ${FILE_CONTENT_MAX_BYTES} bytes`);
  }
}

async function auditFile(
  deps: FileDeps,
  actor: FileLibActor,
  action: string,
  project: ProjectChain,
  filePath: string,
  detail: Record<string, unknown>,
): Promise<void> {
  await deps.prisma.$transaction(async (tx) => {
    await writeFileLibAudit(tx, {
      action,
      actorUserId: actor.userId,
      organizationId: deps.organizationId,
      objectType: "file",
      objectId: project.node.id,
      objectPath: `${project.node.pathIds}:${filePath}`,
      detail,
    });
  });
}

/* ---------------------------------------------------------------- 读操作 */

export async function listFiles(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  prefix?: string,
): Promise<readonly FileEntry[]> {
  const project = await requireProject(deps, actor, projectId, "VIEW");
  return deps.versionStore.list(project.storageDir, prefix === undefined ? undefined : validateFilePath(prefix));
}

export async function readFile(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  rawPath: string,
): Promise<FileContentDto> {
  const filePath = validateFilePath(rawPath);
  const project = await requireProject(deps, actor, projectId, "VIEW");
  const [version, buffer] = await Promise.all([
    deps.versionStore.head(project.storageDir, filePath),
    deps.versionStore.read(project.storageDir, filePath),
  ]);
  const { encoding, content } = encodeContent(buffer);
  return { path: filePath, version, encoding, content, size: buffer.byteLength };
}

/** 原始字节下载(浏览器 save-as 用):JSON 之外的第二条读取通道,同样的 VIEW 门禁。 */
export async function readFileRaw(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  rawPath: string,
): Promise<{ readonly buffer: Buffer; readonly version: string; readonly filename: string }> {
  const filePath = validateFilePath(rawPath);
  const project = await requireProject(deps, actor, projectId, "VIEW");
  const [version, buffer] = await Promise.all([
    deps.versionStore.head(project.storageDir, filePath),
    deps.versionStore.read(project.storageDir, filePath),
  ]);
  return { buffer, version, filename: filePath.split("/").pop() ?? "download" };
}

export async function fileHistory(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  rawPath: string,
  limit?: number,
): Promise<readonly VersionInfo[]> {
  const filePath = validateFilePath(rawPath);
  const project = await requireProject(deps, actor, projectId, "VIEW");
  return deps.versionStore.history(project.storageDir, filePath, limit);
}

export async function diffFile(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  rawPath: string,
  from: string,
  to: string,
): Promise<{ readonly diff: string }> {
  const filePath = validateFilePath(rawPath);
  const project = await requireProject(deps, actor, projectId, "VIEW");
  return { diff: await deps.versionStore.diff(project.storageDir, filePath, from, to) };
}

/* ---------------------------------------------------------------- 写操作 */

export interface CommitInput {
  readonly path: string;
  /** null = 新建(已存在则 409);编辑时传 readFile 拿到的 version。 */
  readonly baseVersion: string | null;
  readonly content: string;
  readonly encoding?: FileContentEncoding | undefined;
  readonly message?: string | undefined;
}

/**
 * 统一写入口(上传/编辑共用):先 commit,成功写 file.commit / file.upload 审计;
 * 冲突写 file.conflict_detected 后抛 409(details 带 currentVersion,编辑 UI 用它拉 diff)。
 */
export async function commitFile(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  input: CommitInput,
): Promise<{ readonly version: string }> {
  const filePath = validateFilePath(input.path);
  const content = decodeContent(input.content, input.encoding ?? "utf8");
  checkSize(content);
  const project = await requireProject(deps, actor, projectId, "EDIT");

  const result: CommitResult = await deps.versionStore.commit(project.storageDir, filePath, {
    baseVersion: input.baseVersion,
    content,
    message: input.message,
    author: actor.userId,
  });

  if (result.status === "conflict") {
    await auditFile(deps, actor, FILE_LIB_AUDIT_ACTIONS.fileConflictDetected, project, filePath, {
      baseVersion: input.baseVersion,
      currentVersion: result.currentVersion,
    });
    throw new FileLibError(409, "version_conflict", "file was modified since baseVersion", {
      currentVersion: result.currentVersion,
    });
  }

  await auditFile(
    deps,
    actor,
    input.baseVersion === null ? FILE_LIB_AUDIT_ACTIONS.fileUpload : FILE_LIB_AUDIT_ACTIONS.fileCommit,
    project,
    filePath,
    { version: result.version, message: input.message ?? null },
  );
  return { version: result.version };
}

export async function deleteFile(
  deps: FileDeps,
  actor: FileLibActor,
  projectId: string,
  rawPath: string,
  baseVersion: string,
): Promise<void> {
  const filePath = validateFilePath(rawPath);
  const project = await requireProject(deps, actor, projectId, "EDIT");
  const result = await deps.versionStore.remove(project.storageDir, filePath, baseVersion);
  if (result.status === "conflict") {
    await auditFile(deps, actor, FILE_LIB_AUDIT_ACTIONS.fileConflictDetected, project, filePath, {
      baseVersion,
      currentVersion: result.currentVersion,
    });
    throw new FileLibError(409, "version_conflict", "file was modified since baseVersion", {
      currentVersion: result.currentVersion,
    });
  }
  await auditFile(deps, actor, FILE_LIB_AUDIT_ACTIONS.fileDelete, project, filePath, { baseVersion });
}
