import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, join, relative } from "node:path";

type JsonRecord = Record<string, unknown>;

const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
  error instanceof Error && "code" in error;

export const ensureDirectory = async (path: string) => {
  await mkdir(path, { recursive: true });
};

export const atomicWriteFile = async (path: string, content: string) => {
  await ensureDirectory(dirname(path));
  const tempPath = `${path}.${process.pid}.${Date.now().toString(36)}.${randomUUID()}.tmp`;
  try {
    await writeFile(tempPath, content, "utf8");
    await rename(tempPath, path);
  } catch (error) {
    await removeFileIfExists(tempPath);
    throw error;
  }
};

type HistoricalWriteOptions = {
  afterWrite?: () => Promise<void> | void;
  backupDir: string;
  rootDir: string;
};

export const atomicWriteFileWithHistory = async (
  path: string,
  content: string,
  options: HistoricalWriteOptions,
) => {
  const previous = await readTextFile(path);
  if (previous === content) {
    return { backupPath: null as string | null, changed: false };
  }

  let backupPath: string | null = null;
  if (previous !== null) {
    // 仅在覆盖已有文件时保留备份版本，避免为首次创建产生空备份。
    backupPath = buildBackupPath(path, options);
    await atomicWriteFile(backupPath, previous);
  }

  try {
    await atomicWriteFile(path, content);
    // 给调用方预留一个写后钩子；若后续步骤失败，这里仍会回滚到旧内容。
    await options.afterWrite?.();
  } catch (error) {
    try {
      if (previous === null) {
        await removeFileIfExists(path);
      } else {
        await atomicWriteFile(path, previous);
      }
    } catch (rollbackError) {
      throw new AggregateError(
        [error, rollbackError],
        `write failed and rollback failed for ${path}`,
      );
    }
    throw error;
  }

  return { backupPath, changed: true };
};

export const atomicWriteJson = async (path: string, value: JsonRecord | unknown[]) => {
  await atomicWriteFile(path, JSON.stringify(value, null, 2));
};

export const readJsonFile = async <T>(path: string): Promise<T | null> => {
  try {
    const content = await readFile(path, "utf8");
    return JSON.parse(content) as T;
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return null;
    }
    throw error;
  }
};

export const readTextFile = async (path: string): Promise<string | null> => {
  try {
    return await readFile(path, "utf8");
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return null;
    }
    throw error;
  }
};

export const listJsonFiles = async (path: string) => {
  try {
    const names = await readdir(path);
    return names.filter((name) => name.endsWith(".json")).map((name) => join(path, name));
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return [];
    }
    throw error;
  }
};

export const listFiles = async (path: string) => {
  try {
    const names = await readdir(path);
    return names.map((name) => join(path, name));
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return [];
    }
    throw error;
  }
};

export const removeFileIfExists = async (path: string) => {
  try {
    await rm(path, { force: true });
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return;
    }
    throw error;
  }
};

export const getFileStat = async (path: string) => {
  try {
    return await stat(path);
  } catch (error) {
    if (isErrnoException(error) && error.code === "ENOENT") {
      return null;
    }
    throw error;
  }
};

export const toScopedKey = (prefix: string, value?: string) => {
  const normalized = value?.trim() || `${prefix}-default`;
  return `${prefix}-${createHash("sha256").update(normalized).digest("hex").slice(0, 16)}`;
};

export const toActorKey = (userId?: string) => toScopedKey("actor", userId);

export const toProjectKey = (projectId?: string) => toScopedKey("project", projectId);

export const toStableId = (...parts: string[]) =>
  createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 24);

export const slugify = (value: string) =>
  value
    .toLowerCase()
    .replace(/[^a-z0-9._-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 64) || "entry";

const buildBackupPath = (path: string, options: HistoricalWriteOptions) => {
  const relativePath = relative(options.rootDir, path);
  const scopedPath =
    relativePath && !relativePath.startsWith("..") ? relativePath : basename(path);
  // 备份目录尽量复用原始相对路径，便于按业务目录回看历史。
  const backupName = `${basename(path)}.${Date.now().toString(36)}.bak`;
  return join(options.backupDir, dirname(scopedPath), backupName);
};
