// Scoped filesystem. Every renderer-supplied path is resolved under
// `app.getPath('userData')`. Post-normalisation we re-check the resolved
// absolute path is still contained in the root; anything that breaks out
// (via .., symlink, absolute path) is rejected. Binary payloads are
// base64 on the wire because JSON IPC can't carry raw bytes cleanly.

import { app, ipcMain } from 'electron';
import { promises as fs } from 'node:fs';
import path from 'node:path';

import { CHANNELS, type FsPath, type FsRenameArgs, type FsWriteArgs } from '../ipc-types';

function rootDir(): string {
  return app.getPath('userData');
}

function resolveScoped(rel: FsPath): string {
  const root = rootDir();
  if (path.isAbsolute(rel)) {
    throw new Error('fs-scoped: absolute path rejected');
  }
  const normalised = path.normalize(rel);
  if (normalised.split(/[\\/]/).includes('..')) {
    throw new Error('fs-scoped: path traversal rejected');
  }
  const abs = path.resolve(root, normalised);
  const withSep = root.endsWith(path.sep) ? root : root + path.sep;
  if (abs !== root && !abs.startsWith(withSep)) {
    throw new Error('fs-scoped: escaped scope');
  }
  return abs;
}

export function register(): void {
  ipcMain.handle(CHANNELS.FS_APP_LOCAL_DATA_DIR, async (): Promise<string> => rootDir());

  ipcMain.handle(CHANNELS.FS_READ, async (_evt, rel: FsPath): Promise<string | null> => {
    const abs = resolveScoped(rel);
    try {
      const buf = await fs.readFile(abs);
      return buf.toString('base64');
    } catch (err: unknown) {
      if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
      throw err;
    }
  });

  ipcMain.handle(CHANNELS.FS_WRITE, async (_evt, args: FsWriteArgs): Promise<void> => {
    const abs = resolveScoped(args.path);
    await fs.mkdir(path.dirname(abs), { recursive: true });
    const buf = Buffer.from(args.dataBase64, 'base64');
    await fs.writeFile(abs, buf);
  });

  ipcMain.handle(CHANNELS.FS_EXISTS, async (_evt, rel: FsPath): Promise<boolean> => {
    const abs = resolveScoped(rel);
    try {
      await fs.access(abs);
      return true;
    } catch {
      return false;
    }
  });

  ipcMain.handle(CHANNELS.FS_MKDIR, async (_evt, rel: FsPath): Promise<void> => {
    const abs = resolveScoped(rel);
    await fs.mkdir(abs, { recursive: true });
  });

  ipcMain.handle(CHANNELS.FS_RENAME, async (_evt, args: FsRenameArgs): Promise<void> => {
    const from = resolveScoped(args.from);
    const to = resolveScoped(args.to);
    await fs.mkdir(path.dirname(to), { recursive: true });
    await fs.rename(from, to);
  });

  ipcMain.handle(CHANNELS.FS_REMOVE, async (_evt, rel: FsPath): Promise<void> => {
    const abs = resolveScoped(rel);
    await fs.rm(abs, { recursive: true, force: true });
  });
}
