/**
 * Minimal in-memory filesystem with the fs.promises surface isomorphic-git
 * needs. Runs identically in Node (tests) and the browser (extension) — no
 * IndexedDB, no LightningFS; a Tier 1 repo lives and dies in memory and is
 * exported as a zip.
 */

interface FileEntry { type: 'file'; data: Uint8Array; mode: number }
interface DirEntry { type: 'dir' }
interface LinkEntry { type: 'symlink'; target: string }
type Entry = FileEntry | DirEntry | LinkEntry;

function errnoError(code: string, path: string): NodeJS.ErrnoException {
  const e = new Error(`${code}: ${path}`) as NodeJS.ErrnoException;
  e.code = code;
  return e;
}

function normalize(p: string): string {
  const parts = p.replace(/\\/g, '/').split('/').filter(s => s && s !== '.');
  const out: string[] = [];
  for (const part of parts) {
    if (part === '..') out.pop();
    else out.push(part);
  }
  return '/' + out.join('/');
}

function parentOf(p: string): string {
  return normalize(p).replace(/\/[^/]*$/, '') || '/';
}

class Stat {
  constructor(private entry: Entry, public size: number) {}
  isFile(): boolean { return this.entry.type === 'file'; }
  isDirectory(): boolean { return this.entry.type === 'dir'; }
  isSymbolicLink(): boolean { return this.entry.type === 'symlink'; }
  get type(): string { return this.entry.type; }
  get mode(): number { return this.entry.type === 'file' ? this.entry.mode : 0o777; }
  get mtimeMs(): number { return 0; }
  get ctimeMs(): number { return 0; }
  get uid(): number { return 1; }
  get gid(): number { return 1; }
  get dev(): number { return 1; }
  get ino(): number { return 1; }
}

export interface MemFsPromises {
  readFile(path: string, opts?: { encoding?: string } | string): Promise<Uint8Array | string>;
  writeFile(path: string, data: Uint8Array | string, opts?: { mode?: number } | string): Promise<void>;
  unlink(path: string): Promise<void>;
  readdir(path: string): Promise<string[]>;
  mkdir(path: string): Promise<void>;
  rmdir(path: string): Promise<void>;
  stat(path: string): Promise<Stat>;
  lstat(path: string): Promise<Stat>;
  readlink(path: string): Promise<string>;
  symlink(target: string, path: string): Promise<void>;
}

export interface MemFs {
  /** fs.promises-shaped API for isomorphic-git. */
  promises: MemFsPromises;
  /** All files (not dirs) as path → bytes, paths relative to `root`. */
  snapshot(root?: string): Map<string, Uint8Array>;
}

export function createMemFs(): MemFs {
  const entries = new Map<string, Entry>([['/', { type: 'dir' }]]);

  const requireDir = (p: string) => {
    const e = entries.get(p);
    if (!e) throw errnoError('ENOENT', p);
    if (e.type !== 'dir') throw errnoError('ENOTDIR', p);
  };

  const promises = {
    async readFile(path: string, opts?: { encoding?: string } | string) {
      const p = normalize(path);
      const e = entries.get(p);
      if (!e) throw errnoError('ENOENT', p);
      if (e.type === 'dir') throw errnoError('EISDIR', p);
      if (e.type === 'symlink') throw errnoError('EINVAL', p);
      const enc = typeof opts === 'string' ? opts : opts?.encoding;
      return enc === 'utf8' ? new TextDecoder().decode(e.data) : e.data;
    },
    async writeFile(path: string, data: Uint8Array | string, opts?: { mode?: number } | string) {
      const p = normalize(path);
      requireDir(parentOf(p));
      const mode = typeof opts === 'object' && opts?.mode ? opts.mode : 0o644;
      const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : new Uint8Array(data);
      entries.set(p, { type: 'file', data: bytes, mode });
    },
    async unlink(path: string) {
      const p = normalize(path);
      if (!entries.has(p)) throw errnoError('ENOENT', p);
      entries.delete(p);
    },
    async readdir(path: string) {
      const p = normalize(path);
      requireDir(p);
      const prefix = p === '/' ? '/' : `${p}/`;
      const names = new Set<string>();
      for (const key of entries.keys()) {
        if (key !== p && key.startsWith(prefix)) {
          names.add(key.slice(prefix.length).split('/')[0]);
        }
      }
      return [...names];
    },
    async mkdir(path: string) {
      const p = normalize(path);
      if (entries.has(p)) throw errnoError('EEXIST', p);
      requireDir(parentOf(p));
      entries.set(p, { type: 'dir' });
    },
    async rmdir(path: string) {
      const p = normalize(path);
      requireDir(p);
      entries.delete(p);
    },
    async stat(path: string) {
      const p = normalize(path);
      // Follow one symlink hop (enough for git's usage here).
      let e = entries.get(p);
      if (e?.type === 'symlink') e = entries.get(normalize(e.target));
      if (!e) throw errnoError('ENOENT', p);
      const size = e.type === 'file' ? e.data.byteLength : 0;
      return new Stat(e, size);
    },
    async lstat(path: string) {
      const p = normalize(path);
      const e = entries.get(p);
      if (!e) throw errnoError('ENOENT', p);
      const size = e.type === 'file' ? e.data.byteLength
        : e.type === 'symlink' ? e.target.length : 0;
      return new Stat(e, size);
    },
    async readlink(path: string) {
      const p = normalize(path);
      const e = entries.get(p);
      if (!e) throw errnoError('ENOENT', p);
      if (e.type !== 'symlink') throw errnoError('EINVAL', p);
      return e.target;
    },
    async symlink(target: string, path: string) {
      const p = normalize(path);
      requireDir(parentOf(p));
      entries.set(p, { type: 'symlink', target });
    },
  };

  return {
    promises,
    snapshot(root = '/') {
      const prefix = normalize(root) === '/' ? '/' : `${normalize(root)}/`;
      const out = new Map<string, Uint8Array>();
      for (const [key, e] of entries) {
        if (e.type === 'file' && key.startsWith(prefix)) {
          out.set(key.slice(prefix.length), e.data);
        }
      }
      return out;
    },
  };
}
