// lib/mines/storage.js
//
// Dataroom storage interface for the Mines module.
//
// This is a SWAPPABLE INTERFACE, not a local-disk contract. Callers use only
// `listFolder(root)`, `readDocument(ref)`, and `statFile(ref)`; they make no
// assumptions about where the bytes live. The default implementation reads from
// the local filesystem, but a future blob/object-store implementation (S3, GCS,
// Azure Blob, a SQLite blob table, …) can replace `createLocalStorage` without
// touching any caller — the import-root allowlist enforcement, dotfile/symlink
// skipping, and secret/system-path blocking all live INSIDE the local impl, not
// in the callers.
//
// `org_id` is threaded so approved-import-root checks are org-scoped: a path is
// readable only if it is inside an OS-allowed root OR an import root approved for
// that org.

import fs from 'node:fs';
import path from 'node:path';
import { isInsideAllowedRoots } from '../tools/safety.js';
import { isInsideApprovedMinesImportRoot } from './import-roots.js';

// Defence-in-depth: never descend into these even within an allowed root.
// Mirrors data-import's walkFiles semantics.
const SKIP_DIR_NAMES = new Set(['node_modules', '.git', '.ssh']);
const SECRET_SEGMENTS = new Set(['.ssh']);

function hasBlockedSegment(absPath) {
  const parts = absPath.split(path.sep).filter(Boolean);
  if (parts.some(part => SECRET_SEGMENTS.has(part))) return true;
  const base = path.basename(absPath);
  return base === '.env.local' || /^\.env($|\.)/.test(base);
}

/**
 * Local-disk implementation of the dataroom storage interface.
 *
 * Every call enforces the import-root allowlist: a `ref` is readable only if it
 * is inside an OS-allowed root (`isInsideAllowedRoots`) OR inside an import root
 * approved for `org_id` (`isInsideApprovedMinesImportRoot`). Anything outside —
 * and explicitly any secret/system path (.env, .ssh, …) — throws an Error whose
 * message starts with `allowlist_violation:`.
 *
 * @param {object} [opts]
 * @param {string} [opts.org_id] org scope for approved-import-root checks
 */
export function createLocalStorage({ org_id } = {}) {
  function assertReadable(inputPath) {
    const abs = path.resolve(String(inputPath || ''));
    if (!inputPath) {
      throw new Error('allowlist_violation: empty path');
    }
    if (hasBlockedSegment(abs)) {
      throw new Error('allowlist_violation: path is a secret/system path: ' + abs);
    }
    if (!isInsideAllowedRoots(abs) && !isInsideApprovedMinesImportRoot(abs, { org_id })) {
      throw new Error('allowlist_violation: path is outside the OS-allowed or approved Mines import roots: ' + abs);
    }
    return abs;
  }

  return {
    /**
     * Recursively collect absolute file paths under `root`. Skips dotfiles,
     * symlinks, and SKIP_DIR_NAMES (node_modules/.git/.ssh). Mirrors
     * data-import's walkFiles semantics. Enforces the allowlist on `root`.
     * @param {string} root
     * @returns {string[]} absolute file paths
     */
    listFolder(root) {
      const absRoot = assertReadable(root);
      const out = [];
      const stack = [absRoot];
      while (stack.length) {
        const dir = stack.pop();
        let entries;
        try {
          entries = fs.readdirSync(dir, { withFileTypes: true });
        } catch {
          continue; // unreadable directory — skip
        }
        for (const entry of entries) {
          const name = entry.name;
          if (name.startsWith('.')) continue;
          const full = path.join(dir, name);
          if (entry.isSymbolicLink()) continue; // avoid cycles / escapes
          if (entry.isDirectory()) {
            if (SKIP_DIR_NAMES.has(name)) continue;
            stack.push(full);
          } else if (entry.isFile()) {
            out.push(full);
          }
        }
      }
      return out;
    },

    /**
     * Read the full bytes of a document.
     * @param {string} ref absolute file path
     * @returns {Buffer}
     */
    readDocument(ref) {
      const abs = assertReadable(ref);
      return fs.readFileSync(abs);
    },

    /**
     * Stat a file/directory. Returns null if it does not exist.
     * @param {string} ref absolute file path
     * @returns {{ size: number, mtimeMs: number, isDirectory: boolean, isFile: boolean } | null}
     */
    statFile(ref) {
      const abs = assertReadable(ref);
      let st;
      try {
        st = fs.statSync(abs);
      } catch {
        return null;
      }
      return {
        size: st.size,
        mtimeMs: st.mtimeMs,
        isDirectory: st.isDirectory(),
        isFile: st.isFile(),
      };
    },
  };
}

// Default singleton (no org scope; approved-import-root checks fall through to
// the default org inside import-roots.js). Org-scoped callers should construct
// their own instance via createLocalStorage({ org_id }).
export const storage = createLocalStorage();

export default storage;
