import { createHash } from "node:crypto";
import { cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import type { PrismaClient } from "@prisma/client";
import { createFolder, createProjectFromOrgAdmin } from "../projectOnboarding.js";

export interface LegacyProjectManifestEntry {
  readonly legacyId: string;
  readonly name: string;
  readonly folderPath: readonly string[];
  readonly sourceRelativePath: string;
}

export interface LegacyProjectImportState {
  readonly version: 1;
  readonly projects: Readonly<Record<string, {
    readonly status: "PENDING" | "COMPLETED";
    readonly projectId: string;
    readonly workspaceDir: string;
    readonly importedAt: string;
    readonly sourceRelativePath: string;
  }>>;
}

export async function importLegacyProjects(input: {
  readonly prisma: PrismaClient;
  readonly organizationId: string;
  readonly actorFeishuOpenId: string;
  readonly workspaceRoot: string;
  readonly sourceRoot: string;
  readonly stateFile: string;
  readonly projects: readonly LegacyProjectManifestEntry[];
  readonly onProgress?: (message: string) => void;
}): Promise<LegacyProjectImportState> {
  const sourceRoot = await realpath(input.sourceRoot);
  const state = await readState(input.stateFile);
  const projects = { ...state.projects };
  const seen = new Set<string>();
  for (const entry of input.projects) {
    validateEntry(entry);
    if (seen.has(entry.legacyId)) throw new Error(`duplicate legacy project id: ${entry.legacyId}`);
    seen.add(entry.legacyId);
    const sourceDir = await confinedSourceDir(sourceRoot, entry.sourceRelativePath);
    const projectId = importedProjectId(input.organizationId, entry.legacyId);
    const existing = await input.prisma.project.findUnique({ where: { id: projectId } });
    const recorded = projects[entry.legacyId];
    if (recorded !== undefined && (recorded.projectId !== projectId || recorded.sourceRelativePath !== entry.sourceRelativePath)) {
      throw new Error(`legacy import state identity conflict: ${entry.legacyId}`);
    }
    if (recorded?.status === "COMPLETED" && existing === null) {
      throw new Error(`legacy import state references missing project: ${entry.legacyId} -> ${recorded.projectId}`);
    }
    if (existing !== null) {
      if (existing.organizationId !== input.organizationId || (recorded !== undefined && recorded.projectId !== existing.id)) {
        throw new Error(`legacy import identity conflict: ${entry.legacyId} -> ${existing.id}`);
      }
      if (await hasCompletionMarker(existing.workspaceDir, entry)) {
        await ensureImportAudit(input.prisma, existing.id, entry);
        projects[entry.legacyId] = {
          status: "COMPLETED",
          projectId: existing.id,
          workspaceDir: existing.workspaceDir,
          importedAt: recorded?.importedAt || new Date().toISOString(),
          sourceRelativePath: entry.sourceRelativePath,
        };
        await writeState(input.stateFile, { version: 1, projects });
        input.onProgress?.(`skip ${entry.legacyId}: recovered completed import`);
        continue;
      }
      if (recorded?.status !== "PENDING") {
        throw new Error(`refusing to remove legacy project without a matching pending record: ${entry.legacyId}`);
      }
      input.onProgress?.(`recover ${entry.legacyId}: remove incomplete target`);
      await removeIncompleteTarget(input.prisma, existing.id, existing.workspaceDir, input.workspaceRoot);
    }
    projects[entry.legacyId] = {
      status: "PENDING",
      projectId,
      workspaceDir: "",
      importedAt: "",
      sourceRelativePath: entry.sourceRelativePath,
    };
    await writeState(input.stateFile, { version: 1, projects });
    const folderId = await ensureFolderPath(input.prisma, input.organizationId, ["旧教学资产", ...entry.folderPath]);
    input.onProgress?.(`import ${entry.legacyId}: ${entry.name}`);
    const created = await createProjectFromOrgAdmin(input.prisma, {
      organizationId: input.organizationId,
      actorFeishuOpenId: input.actorFeishuOpenId,
      name: entry.name,
      workspaceRoot: input.workspaceRoot,
      folderId,
      projectId,
    });
    try {
      await copyLegacyProject(sourceDir, created.workspaceDir, entry);
    } catch (error) {
      try {
        await removeIncompleteTarget(input.prisma, created.projectId, created.workspaceDir, input.workspaceRoot);
        delete projects[entry.legacyId];
        await writeState(input.stateFile, { version: 1, projects });
      } catch (cleanupError) {
        throw new AggregateError(
          [error, cleanupError],
          `legacy project import and target cleanup failed: ${entry.legacyId}`,
        );
      }
      throw new Error(`legacy project import failed: ${entry.legacyId}: ${errorMessage(error)}`, { cause: error });
    }
    await ensureImportAudit(input.prisma, created.projectId, entry);
    projects[entry.legacyId] = {
      status: "COMPLETED",
      projectId: created.projectId,
      workspaceDir: created.workspaceDir,
      importedAt: new Date().toISOString(),
      sourceRelativePath: entry.sourceRelativePath,
    };
    await writeState(input.stateFile, { version: 1, projects });
  }
  return { version: 1, projects };
}

async function ensureFolderPath(
  prisma: PrismaClient,
  organizationId: string,
  parts: readonly string[],
): Promise<string> {
  let parentId: string | undefined;
  for (const name of parts) {
    const existing = await prisma.folder.findFirst({
      where: { organizationId, parentId: parentId ?? null, name, archivedAt: null },
      select: { id: true },
    });
    if (existing !== null) {
      parentId = existing.id;
      continue;
    }
    const created = await createFolder(prisma, {
      organizationId,
      name,
      ...(parentId !== undefined ? { parentId } : {}),
    });
    parentId = created.id;
  }
  if (parentId === undefined) throw new Error("legacy import folder path is empty");
  return parentId;
}

async function copyLegacyProject(
  sourceDir: string,
  workspaceDir: string,
  entry: LegacyProjectManifestEntry,
): Promise<void> {
  const sourceWorkspace = join(sourceDir, "workspace");
  await assertNoSymlinks(sourceWorkspace);
  const names = await readdir(sourceWorkspace);
  for (const name of names) {
    if (name === ".claude" || name === ".cph") continue;
    await cp(join(sourceWorkspace, name), join(workspaceDir, name), {
      recursive: true,
      force: false,
      errorOnExist: true,
      preserveTimestamps: true,
      filter: (source) => {
        const parts = relative(sourceWorkspace, source).split(sep);
        return !parts.includes(".claude") && !parts.includes(".cph");
      },
    });
  }
  const legacyDir = join(workspaceDir, ".legacy-source");
  await mkdir(legacyDir, { mode: 0o750 });
  await cp(join(sourceDir, "project.json"), join(legacyDir, "project.json"), {
    force: false,
    errorOnExist: true,
    preserveTimestamps: true,
  });
  const rawDir = join(sourceDir, "_raw");
  await assertNoSymlinks(rawDir).catch((error: unknown) => {
    if (isMissing(error)) return;
    throw error;
  });
  await cp(rawDir, join(legacyDir, "raw"), {
    recursive: true,
    force: false,
    errorOnExist: true,
    preserveTimestamps: true,
  }).catch((error: unknown) => {
    if (isMissing(error)) return;
    throw error;
  });
  await writeFile(join(legacyDir, "migration.json"), `${JSON.stringify({
    source: "teaching-material-host-service",
    legacyProjectId: entry.legacyId,
    legacyPath: entry.sourceRelativePath,
    migratedAt: new Date().toISOString(),
  }, null, 2)}\n`, { mode: 0o640 });
}

function importedProjectId(organizationId: string, legacyId: string): string {
  const digest = createHash("sha256")
    .update("teaching-material-host-service\0")
    .update(organizationId)
    .update("\0")
    .update(legacyId)
    .digest("hex")
    .slice(0, 32);
  return `legacy_${digest}`;
}

async function hasCompletionMarker(
  workspaceDir: string,
  entry: LegacyProjectManifestEntry,
): Promise<boolean> {
  try {
    const marker = JSON.parse(await readFile(join(workspaceDir, ".legacy-source", "migration.json"), "utf8")) as unknown;
    return typeof marker === "object" && marker !== null
      && "legacyProjectId" in marker && marker.legacyProjectId === entry.legacyId
      && "legacyPath" in marker && marker.legacyPath === entry.sourceRelativePath;
  } catch (error) {
    if (isMissing(error)) return false;
    if (error instanceof SyntaxError) {
      throw new Error(`invalid legacy completion marker: ${workspaceDir}`, { cause: error });
    }
    throw error;
  }
}

async function ensureImportAudit(
  prisma: PrismaClient,
  projectId: string,
  entry: LegacyProjectManifestEntry,
): Promise<void> {
  const existing = await prisma.auditEntry.findFirst({
    where: { projectId, action: "legacy_project.imported" },
    select: { id: true },
  });
  if (existing !== null) return;
  await prisma.auditEntry.create({
    data: {
      projectId,
      action: "legacy_project.imported",
      metadata: {
        source: "teaching-material-host-service",
        legacyProjectId: entry.legacyId,
        legacyPath: entry.sourceRelativePath,
      },
    },
  });
}

async function removeIncompleteTarget(
  prisma: PrismaClient,
  projectId: string,
  workspaceDir: string,
  workspaceRoot: string,
): Promise<void> {
  await assertConfinedExistingPath(workspaceRoot, workspaceDir);
  const failures: unknown[] = [];
  try {
    await prisma.project.delete({ where: { id: projectId } });
  } catch (error) {
    failures.push(error);
  }
  try {
    await rm(workspaceDir, { recursive: true, force: true });
  } catch (error) {
    failures.push(error);
  }
  if (failures.length > 0) throw new AggregateError(failures, `failed to remove incomplete legacy target: ${projectId}`);
}

async function assertConfinedExistingPath(root: string, path: string): Promise<void> {
  const trustedRoot = await realpath(root);
  const candidate = await realpath(path);
  const rel = relative(trustedRoot, candidate);
  if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
    throw new Error(`refusing to remove path outside workspace root: ${path}`);
  }
}

async function assertNoSymlinks(path: string): Promise<void> {
  const metadata = await lstat(path);
  if (metadata.isSymbolicLink()) throw new Error(`legacy import rejects symbolic link: ${path}`);
  if (!metadata.isDirectory()) return;
  for (const entry of await readdir(path)) await assertNoSymlinks(join(path, entry));
}

async function confinedSourceDir(sourceRoot: string, relativePath: string): Promise<string> {
  const candidate = await realpath(resolve(sourceRoot, relativePath));
  const rel = relative(sourceRoot, candidate);
  if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
    throw new Error(`legacy source path escapes source root: ${relativePath}`);
  }
  return candidate;
}

function validateEntry(entry: LegacyProjectManifestEntry): void {
  if (typeof entry !== "object" || entry === null) throw new Error("invalid legacy project entry");
  if (typeof entry.legacyId !== "string") throw new Error("legacy project id must be a string");
  if (!/^[A-Za-z0-9_-]+$/.test(entry.legacyId)) throw new Error(`invalid legacy project id: ${entry.legacyId}`);
  if (typeof entry.name !== "string") throw new Error(`legacy project name must be a string: ${entry.legacyId}`);
  if (entry.name.trim() === "") throw new Error(`legacy project name is empty: ${entry.legacyId}`);
  if (typeof entry.sourceRelativePath !== "string") {
    throw new Error(`legacy source path must be a string: ${entry.legacyId}`);
  }
  if (entry.sourceRelativePath === "" || resolve("/", entry.sourceRelativePath) === "/") {
    throw new Error(`invalid legacy source path: ${entry.legacyId}`);
  }
  if (!Array.isArray(entry.folderPath)) throw new Error(`legacy folder path must be an array: ${entry.legacyId}`);
  for (const part of entry.folderPath) {
    if (typeof part !== "string" || part.trim() === "" || part === "." || part === ".." || part.includes("/") || part.includes("\\")) {
      throw new Error(`invalid legacy folder part for ${entry.legacyId}: ${part}`);
    }
  }
}

async function readState(path: string): Promise<LegacyProjectImportState> {
  try {
    const parsed = JSON.parse(await readFile(path, "utf8")) as LegacyProjectImportState;
    if (parsed.version !== 1 || typeof parsed.projects !== "object" || parsed.projects === null) {
      throw new Error(`invalid legacy import state: ${path}`);
    }
    for (const [legacyId, record] of Object.entries(parsed.projects)) validateStateRecord(path, legacyId, record);
    return parsed;
  } catch (error) {
    if (isMissing(error)) return { version: 1, projects: {} };
    throw error;
  }
}

function validateStateRecord(path: string, legacyId: string, record: unknown): void {
  if (typeof record !== "object" || record === null || Array.isArray(record)) {
    throw new Error(`invalid legacy import state record: ${path}#${legacyId}`);
  }
  const values = record as Record<string, unknown>;
  const expectedKeys = ["importedAt", "projectId", "sourceRelativePath", "status", "workspaceDir"];
  if (Object.keys(values).sort().join("\0") !== expectedKeys.join("\0")) {
    throw new Error(`invalid legacy import state fields: ${path}#${legacyId}`);
  }
  if (values.status !== "PENDING" && values.status !== "COMPLETED") {
    throw new Error(`invalid legacy import state status: ${path}#${legacyId}`);
  }
  for (const field of ["projectId", "workspaceDir", "importedAt", "sourceRelativePath"] as const) {
    if (typeof values[field] !== "string") throw new Error(`invalid legacy import state ${field}: ${path}#${legacyId}`);
  }
  if (values.projectId === "" || values.sourceRelativePath === "") {
    throw new Error(`invalid legacy import state identity: ${path}#${legacyId}`);
  }
  if (values.status === "PENDING" && (values.workspaceDir !== "" || values.importedAt !== "")) {
    throw new Error(`invalid pending legacy import state: ${path}#${legacyId}`);
  }
  if (values.status === "COMPLETED" && (values.workspaceDir === "" || values.importedAt === "")) {
    throw new Error(`invalid completed legacy import state: ${path}#${legacyId}`);
  }
}

async function writeState(path: string, state: LegacyProjectImportState): Promise<void> {
  await mkdir(dirname(path), { recursive: true, mode: 0o750 });
  const temporary = `${path}.tmp`;
  await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
  await rename(temporary, path);
}

function isMissing(error: unknown): boolean {
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}
