import * as FileSystem from 'expo-file-system';

import { errorMessage, warnDev } from './runtimeUtils';

const fileLocks = new Map<string, Promise<void>>();

/** Serialize callers sharing a key and release the map entry after the tail. */
export function withFileLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
  const previous = fileLocks.get(key) || Promise.resolve();
  const run = previous.catch(() => undefined).then(fn);
  const tracked = run.then(() => undefined, () => undefined);
  fileLocks.set(key, tracked);

  return run.finally(() => {
    if (fileLocks.get(key) === tracked) {
      fileLocks.delete(key);
    }
  });
}

async function fileExists(path: string): Promise<boolean> {
  try {
    return (await FileSystem.getInfoAsync(path)).exists;
  } catch {
    return false;
  }
}

async function deleteIfExists(path: string): Promise<void> {
  try {
    await FileSystem.deleteAsync(path, { idempotent: true });
  } catch (error) {
    warnDev('[PlantIntelligence] temporary file cleanup failed', {
      path,
      message: errorMessage(error),
    });
  }
}

async function parseJsonFile<T>(path: string): Promise<T> {
  return JSON.parse(await FileSystem.readAsStringAsync(path)) as T;
}

async function promoteValidCandidate<T>(
  candidatePath: string,
  targetPath: string,
): Promise<T | undefined> {
  if (!(await fileExists(candidatePath))) return undefined;
  const value = await parseJsonFile<T>(candidatePath);
  await deleteIfExists(targetPath);
  await FileSystem.moveAsync({ from: candidatePath, to: targetPath });
  return value;
}

/**
 * Read a transactional JSON file. The target is authoritative when valid. If a
 * write was interrupted after the previous target became a backup, a complete
 * temp file is promoted first; otherwise the backup is restored.
 */
export async function readJsonFile<T>(path: string, fallback: T): Promise<T> {
  if (!FileSystem.documentDirectory) return fallback;

  const temporaryPath = `${path}.tmp`;
  const backupPath = `${path}.bak`;
  const targetExists = await fileExists(path);

  if (targetExists) {
    try {
      const parsed = await parseJsonFile<T>(path);
      await deleteIfExists(temporaryPath);
      await deleteIfExists(backupPath);
      return parsed;
    } catch (targetError) {
      // A validated temp file can coexist with an old/corrupt target when the
      // app is interrupted just before target -> backup. Prefer that newest
      // complete candidate before falling back to the previous backup.
      try {
        const temporaryValue = await promoteValidCandidate<T>(temporaryPath, path);
        if (temporaryValue !== undefined) {
          await deleteIfExists(backupPath);
          warnDev('[PlantIntelligence] replaced invalid target with completed temporary file', {
            path,
            message: errorMessage(targetError),
          });
          return temporaryValue;
        }
      } catch (temporaryError) {
        warnDev('[PlantIntelligence] temporary storage candidate was unreadable', {
          path,
          message: errorMessage(temporaryError),
        });
        await deleteIfExists(temporaryPath);
      }

      if (await fileExists(backupPath)) {
        try {
          const backupValue = await promoteValidCandidate<T>(backupPath, path);
          if (backupValue === undefined) return fallback;
          await deleteIfExists(temporaryPath);
          warnDev('[PlantIntelligence] restored storage backup after invalid target', {
            path,
            message: errorMessage(targetError),
          });
          return backupValue;
        } catch (backupError) {
          warnDev('[PlantIntelligence] storage target and backup are unreadable', {
            path,
            targetMessage: errorMessage(targetError),
            backupMessage: errorMessage(backupError),
          });
          return fallback;
        }
      }

      warnDev('[PlantIntelligence] storage read failed', {
        path,
        message: errorMessage(targetError),
      });
      return fallback;
    }
  }

  if (await fileExists(temporaryPath)) {
    try {
      const temporaryValue = await promoteValidCandidate<T>(temporaryPath, path);
      if (temporaryValue === undefined) return fallback;
      await deleteIfExists(backupPath);
      warnDev('[PlantIntelligence] recovered completed temporary storage file', { path });
      return temporaryValue;
    } catch (error) {
      warnDev('[PlantIntelligence] temporary storage recovery failed', {
        path,
        message: errorMessage(error),
      });
      await deleteIfExists(temporaryPath);
    }
  }

  if (await fileExists(backupPath)) {
    try {
      const backupValue = await promoteValidCandidate<T>(backupPath, path);
      if (backupValue === undefined) return fallback;
      warnDev('[PlantIntelligence] restored storage backup after interrupted write', { path });
      return backupValue;
    } catch (error) {
      warnDev('[PlantIntelligence] storage backup recovery failed', {
        path,
        message: errorMessage(error),
      });
    }
  }

  return fallback;
}

/**
 * Replace a JSON file without relying on moveAsync to overwrite an existing
 * destination. The old target is retained as a recoverable backup until the
 * validated temp file has become the new target.
 */
export async function writeJsonFile<T>(
  directory: string,
  path: string,
  value: T,
): Promise<void> {
  if (!FileSystem.documentDirectory) return;

  const directoryInfo = await FileSystem.getInfoAsync(directory);
  if (!directoryInfo.exists) {
    await FileSystem.makeDirectoryAsync(directory, { intermediates: true });
  }

  const serialized = JSON.stringify(value);
  if (serialized === undefined) {
    throw new Error('Value is not JSON serializable');
  }

  const temporaryPath = `${path}.tmp`;
  const backupPath = `${path}.bak`;
  await deleteIfExists(temporaryPath);
  await FileSystem.writeAsStringAsync(temporaryPath, serialized);
  await parseJsonFile<unknown>(temporaryPath);

  let movedCurrentToBackup = false;
  try {
    await deleteIfExists(backupPath);
    if (await fileExists(path)) {
      await FileSystem.moveAsync({ from: path, to: backupPath });
      movedCurrentToBackup = true;
    }

    await FileSystem.moveAsync({ from: temporaryPath, to: path });
    await deleteIfExists(backupPath);
  } catch (error) {
    if (!(await fileExists(path)) && movedCurrentToBackup && await fileExists(backupPath)) {
      try {
        await FileSystem.moveAsync({ from: backupPath, to: path });
      } catch (restoreError) {
        warnDev('[PlantIntelligence] storage rollback failed', {
          path,
          message: errorMessage(restoreError),
        });
      }
    }
    throw error;
  } finally {
    await deleteIfExists(temporaryPath);
  }
}
