import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';

import {
  PROJECT_ZIP_EXTENSION,
  PROJECT_ZIP_EXTENSION_LEGACY,
  isProjectZipFileName,
} from '../../shared/project/projectZipExtension';

import { readProjectJsonFromZip } from './yauzlProjectZip';

/**
 * Подменяет файл `finalPath` готовым `completedSrc` (обычно `*.ttrpg.zip.tmp` / `*.dnd.zip.tmp`).
 * Нельзя сначала удалять `finalPath`: при сбое rename после rm проект теряется (Windows/антивирус).
 */
export async function replaceFileAtomic(completedSrc: string, finalPath: string): Promise<void> {
  if (completedSrc === finalPath) return;

  const stSrc = await fs.stat(completedSrc).catch(() => null);
  if (!stSrc?.isFile() || stSrc.size === 0) {
    throw new Error(`replaceFileAtomic: нет или пустой источник: ${completedSrc}`);
  }

  try {
    await fs.rename(completedSrc, finalPath);
    return;
  } catch (first: unknown) {
    const c = (first as NodeJS.ErrnoException).code;
    if (c === 'EXDEV') {
      await fs.copyFile(completedSrc, finalPath);
      await fs.unlink(completedSrc).catch(() => undefined);
      return;
    }
    // Windows: чаще всего EEXIST — целевой файл есть, rename не перезаписывает; идём через backup ниже.
  }

  const stFinal = await fs.stat(finalPath).catch(() => null);
  if (!stFinal?.isFile()) {
    try {
      await fs.rename(completedSrc, finalPath);
      return;
    } catch (again: unknown) {
      throw again instanceof Error ? again : new Error(String(again));
    }
  }

  const backupPath = `${finalPath}.bak.${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}`;
  await fs.rename(finalPath, backupPath);
  try {
    await fs.rename(completedSrc, finalPath);
  } catch (place: unknown) {
    await fs.rename(backupPath, finalPath).catch(() => undefined);
    throw place instanceof Error ? place : new Error(String(place));
  }
  await fs.unlink(backupPath).catch(() => undefined);
}

const ZIP_TMP_SUFFIXES = [`${PROJECT_ZIP_EXTENSION}.tmp`, `${PROJECT_ZIP_EXTENSION_LEGACY}.tmp`] as const;

/** Если сохранение оборвалось, остаётся `*.ttrpg.zip.tmp` / `*.dnd.zip.tmp` — восстанавливаем финальный архив. */
export async function recoverOrphanProjectZipTmpInRoot(root: string): Promise<void> {
  let names: string[];
  try {
    names = await fs.readdir(root);
  } catch {
    return;
  }
  for (const name of names) {
    const matchedSuffix = ZIP_TMP_SUFFIXES.find((s) => name.endsWith(s));
    if (!matchedSuffix) continue;
    const tmpPath = path.join(root, name);
    const finalName = name.slice(0, -'.tmp'.length);
    if (!isProjectZipFileName(finalName)) continue;
    const finalPath = path.join(root, finalName);
    try {
      await fs.access(finalPath);
      continue;
    } catch {
      /* final отсутствует — пробуем поднять из tmp */
    }
    try {
      const st = await fs.stat(tmpPath);
      if (!st.isFile() || st.size < 22) continue;
      await readProjectJsonFromZip(tmpPath);
      await fs.rename(tmpPath, finalPath);
    } catch {
      /* битый tmp не трогаем */
    }
  }
}

/** @deprecated Используйте {@link recoverOrphanProjectZipTmpInRoot}. */
export const recoverOrphanDndZipTmpInRoot = recoverOrphanProjectZipTmpInRoot;
