import * as fs from 'fs';
import * as path from 'path';

const STALE_TIMEOUT_MS = 30_000;
const ACQUIRE_TIMEOUT_MS = 5_000;
const RETRY_INTERVAL_MS = 20;

export interface FileLockOptions {
  acquireTimeoutMs?: number;
  staleTimeoutMs?: number;
  retryIntervalMs?: number;
}

interface LockInfo {
  pid: number | null;
  ageMs: number | null;
}

function readLockInfo(lockPath: string): LockInfo {
  let pid: number | null = null;
  let ageMs: number | null = null;
  try {
    const content = fs.readFileSync(lockPath, 'utf8');
    const lines = content.split('\n');
    const parsedPid = parseInt(lines[0] ?? '', 10);
    if (Number.isFinite(parsedPid) && parsedPid > 0) {
      pid = parsedPid;
    }
    const ts = parseInt(lines[1] ?? '', 10);
    if (Number.isFinite(ts)) {
      ageMs = Date.now() - ts;
    }
  } catch {
    /* lock may have been released concurrently */
  }
  if (ageMs === null) {
    try {
      ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
    } catch {
      /* lock may have been released concurrently */
    }
  }
  return { pid, ageMs };
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    return code !== 'ESRCH';
  }
}

function shouldBreakExistingLock(lockPath: string, staleTimeoutMs: number): boolean {
  const info = readLockInfo(lockPath);
  if (info.pid !== null && !isProcessAlive(info.pid)) {
    return true;
  }
  return info.ageMs !== null && info.ageMs > staleTimeoutMs;
}

function removeLockPath(lockPath: string): void {
  try {
    fs.rmSync(lockPath, { recursive: true, force: true });
  } catch {
    /* another process may have cleaned it */
  }
}

function writeLockFile(lockPath: string): void {
  const fd = fs.openSync(lockPath, 'wx');
  let closeError: unknown = null;
  try {
    fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`);
  } finally {
    try {
      fs.closeSync(fd);
    } catch (err) {
      closeError = err;
    }
  }
  if (closeError) {
    throw closeError instanceof Error ? closeError : new Error('Failed to close file lock');
  }
}

function isExistingLockError(code: string | undefined): boolean {
  return code === 'EEXIST' || code === 'EISDIR';
}

function tryAcquire(lockPath: string, options: Required<FileLockOptions>): boolean {
  try {
    // Fast path: assume the lock directory already exists (the common case once a
    // team dir is created). This drops an existsSync(dir) stat from EVERY acquire,
    // which adds up across the many lock cycles during a team launch.
    writeLockFile(lockPath);
    return true;
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code;
    if (code === 'ENOENT') {
      // Lock directory missing - create it lazily and acquire in the same call, so
      // first-acquire latency in a fresh dir is unchanged.
      try {
        fs.mkdirSync(path.dirname(lockPath), { recursive: true });
        writeLockFile(lockPath);
        return true;
      } catch (retryError) {
        const retryCode = (retryError as NodeJS.ErrnoException).code;
        if (retryCode === 'ENOENT') {
          return false;
        }
        if (isExistingLockError(retryCode)) {
          if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) {
            removeLockPath(lockPath);
          }
          return false;
        }
        throw retryError;
      }
    }
    if (isExistingLockError(code)) {
      if (shouldBreakExistingLock(lockPath, options.staleTimeoutMs)) {
        removeLockPath(lockPath);
      }
      return false;
    }
    throw err;
  }
}

function releaseLock(lockPath: string): void {
  try {
    fs.unlinkSync(lockPath);
  } catch {
    /* already released or cleaned up */
  }
}

function sleepSync(ms: number): void {
  const deadline = Date.now() + ms;
  while (Date.now() < deadline) {
    // Synchronous callers need the same cross-process lock as controller writes.
  }
}

function resolveLockOptions(options: FileLockOptions): Required<FileLockOptions> {
  return {
    acquireTimeoutMs: options.acquireTimeoutMs ?? ACQUIRE_TIMEOUT_MS,
    staleTimeoutMs: options.staleTimeoutMs ?? STALE_TIMEOUT_MS,
    retryIntervalMs: options.retryIntervalMs ?? RETRY_INTERVAL_MS,
  };
}

export function withFileLockSync<T>(
  filePath: string,
  fn: () => T,
  options: FileLockOptions = {}
): T {
  const resolvedOptions = resolveLockOptions(options);
  const lockPath = `${filePath}.lock`;
  const deadline = Date.now() + resolvedOptions.acquireTimeoutMs;

  while (!tryAcquire(lockPath, resolvedOptions)) {
    if (Date.now() >= deadline) {
      throw new Error(`File lock timeout: ${filePath}`);
    }
    sleepSync(Math.min(resolvedOptions.retryIntervalMs, Math.max(0, deadline - Date.now())));
  }

  try {
    return fn();
  } finally {
    releaseLock(lockPath);
  }
}

export async function withFileLock<T>(
  filePath: string,
  fn: () => Promise<T>,
  options: FileLockOptions = {}
): Promise<T> {
  const resolvedOptions = resolveLockOptions(options);
  const lockPath = `${filePath}.lock`;
  const deadline = Date.now() + resolvedOptions.acquireTimeoutMs;

  while (!tryAcquire(lockPath, resolvedOptions)) {
    if (Date.now() >= deadline) {
      throw new Error(`File lock timeout: ${filePath}`);
    }
    await new Promise((resolve) => setTimeout(resolve, resolvedOptions.retryIntervalMs));
  }

  try {
    return await fn();
  } finally {
    releaseLock(lockPath);
  }
}
