// src/index.ts
var TEXT_ENCODER = new TextEncoder();
var TEXT_DECODER = new TextDecoder();
var BrowserVfsError = class extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
    this.name = "BrowserVfsError";
  }
  code;
};
function splitPath(path) {
  return path.split("/").filter(Boolean);
}
function normalizeWorkspacePath(input) {
  const trimmed = input.trim();
  if (!trimmed) throw new BrowserVfsError("invalid_path", "Workspace path is required");
  if (trimmed === "/") return "/";
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) throw new BrowserVfsError("invalid_path", "Workspace path must not be a URL");
  if (trimmed.includes("\\")) throw new BrowserVfsError("invalid_path", "Workspace path must use forward slashes");
  const segments = splitPath(trimmed);
  if (segments.length === 0) throw new BrowserVfsError("invalid_path", "Workspace path is required");
  for (const segment of segments) {
    if (segment === "." || segment === "..") throw new BrowserVfsError("path_traversal", "Workspace path must not traverse directories");
    if (!/^[A-Za-z0-9._@=,+~ -]+$/.test(segment)) throw new BrowserVfsError("invalid_path", "Workspace path contains unsupported characters");
  }
  return "/" + segments.join("/");
}
function projectWorkspaceRoot(projectId) {
  const normalized = normalizeWorkspacePath("/projects/" + projectId);
  const segments = splitPath(normalized);
  if (segments.length !== 2 || segments[0] !== "projects") throw new BrowserVfsError("invalid_project_id", "Project id must be a single safe path segment");
  return normalized;
}
function pathWithinPrefix(path, prefix) {
  return path === prefix || path.startsWith(prefix.endsWith("/") ? prefix : prefix + "/");
}
async function sha256Hex(data) {
  const buffer = new ArrayBuffer(data.byteLength);
  new Uint8Array(buffer).set(data);
  try {
    const digest = await crypto.subtle.digest("SHA-256", buffer);
    return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
  } catch {
    let hash = 2166136261;
    for (const byte of data) {
      hash ^= byte;
      hash = Math.imul(hash, 16777619) >>> 0;
    }
    return hash.toString(16).padStart(8, "0");
  }
}
function toBytes(data) {
  return typeof data === "string" ? TEXT_ENCODER.encode(data) : data;
}
function bytesToBase64(data) {
  let binary = "";
  for (const byte of data) binary += String.fromCharCode(byte);
  return btoa(binary);
}
function base64ToBytes(data) {
  const binary = atob(data);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
  return bytes;
}
function fileStat(path, data, options = {}) {
  return sha256Hex(data).then((etag) => {
    const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
    return {
      path,
      size: data.byteLength,
      etag,
      revision: updatedAt + "-" + etag.slice(0, 12),
      updatedAt,
      ...options.contentType ? { contentType: options.contentType } : {}
    };
  });
}
function directoryEntriesFor(files, prefix) {
  const directories = /* @__PURE__ */ new Set();
  const root = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
  for (const file of files) {
    if (!pathWithinPrefix(file.path, root)) continue;
    const relative = root === "/" ? file.path.slice(1) : file.path.slice(root.length + 1);
    const [first] = relative.split("/");
    if (first && relative.includes("/")) directories.add((root === "/" ? "" : root) + "/" + first);
  }
  const updatedAt = (/* @__PURE__ */ new Date(0)).toISOString();
  return Array.from(directories, (path) => ({ kind: "directory", path, size: 0, etag: "", revision: "", updatedAt })).sort((a, b) => a.path.localeCompare(b.path));
}
var MemoryWorkspaceBackend = class {
  files = /* @__PURE__ */ new Map();
  async stat(path) {
    return this.files.get(normalizeWorkspacePath(path))?.metadata ?? null;
  }
  async readFile(path) {
    const file = this.files.get(normalizeWorkspacePath(path));
    return file ? new Uint8Array(file.body) : null;
  }
  async writeFile(path, data, options = {}) {
    const normalized = normalizeWorkspacePath(path);
    if (normalized === "/") throw new BrowserVfsError("invalid_path", "Workspace file path must not be root");
    const body = new Uint8Array(data);
    const metadata = await fileStat(normalized, body, options);
    this.files.set(normalized, { body, metadata });
    return metadata;
  }
  async deleteFile(path) {
    return this.files.delete(normalizeWorkspacePath(path));
  }
  async listFiles(prefix = "/") {
    const normalized = normalizeWorkspacePath(prefix);
    const files = Array.from(this.files.values(), (file) => ({ ...file.metadata, kind: "file" }));
    const directFiles = files.filter((file) => {
      if (!pathWithinPrefix(file.path, normalized)) return false;
      const relative = normalized === "/" ? file.path.slice(1) : file.path.slice(normalized.length + 1);
      return relative.length > 0 && !relative.includes("/");
    });
    return [...directoryEntriesFor(files, normalized), ...directFiles].sort((a, b) => a.path.localeCompare(b.path));
  }
  async clear() {
    this.files.clear();
  }
};
var OpfsWorkspaceBackend = class _OpfsWorkspaceBackend {
  constructor(root) {
    this.root = root;
  }
  root;
  index = null;
  static async create(options = {}) {
    const scope = options.globalScope ?? globalThis;
    const getDirectory = scope.navigator?.storage?.getDirectory;
    if (typeof getDirectory !== "function") throw new BrowserVfsError("opfs_unavailable", "OPFS is not available");
    const storageRoot = await getDirectory.call(scope.navigator.storage);
    const namespaceRoot = await storageRoot.getDirectoryHandle(options.namespace ?? "default", { create: true });
    return new _OpfsWorkspaceBackend(namespaceRoot);
  }
  async filesDirectory() {
    return this.root.getDirectoryHandle("files", { create: true });
  }
  async readIndex() {
    if (this.index) return this.index;
    try {
      const handle = await this.root.getFileHandle("index.json");
      const file = await handle.getFile();
      const json = TEXT_DECODER.decode(await file.arrayBuffer());
      const records = JSON.parse(json);
      this.index = new Map(records.map((record) => [record.path, record]));
    } catch {
      this.index = /* @__PURE__ */ new Map();
    }
    return this.index;
  }
  async writeIndex() {
    const index = await this.readIndex();
    const handle = await this.root.getFileHandle("index.json", { create: true });
    const writable = await handle.createWritable();
    await writable.write(TEXT_ENCODER.encode(JSON.stringify(Array.from(index.values()).sort((a, b) => a.path.localeCompare(b.path)))));
    await writable.close();
  }
  fileName(path) {
    return encodeURIComponent(path);
  }
  async stat(path) {
    return (await this.readIndex()).get(normalizeWorkspacePath(path)) ?? null;
  }
  async readFile(path) {
    const normalized = normalizeWorkspacePath(path);
    if (!(await this.readIndex()).has(normalized)) return null;
    try {
      const handle = await (await this.filesDirectory()).getFileHandle(this.fileName(normalized));
      const file = await handle.getFile();
      return new Uint8Array(await file.arrayBuffer());
    } catch {
      return null;
    }
  }
  async writeFile(path, data, options = {}) {
    const normalized = normalizeWorkspacePath(path);
    if (normalized === "/") throw new BrowserVfsError("invalid_path", "Workspace file path must not be root");
    const body = new Uint8Array(data);
    const metadata = await fileStat(normalized, body, options);
    const handle = await (await this.filesDirectory()).getFileHandle(this.fileName(normalized), { create: true });
    const writable = await handle.createWritable();
    await writable.write(body);
    await writable.close();
    (await this.readIndex()).set(normalized, metadata);
    await this.writeIndex();
    return metadata;
  }
  async deleteFile(path) {
    const normalized = normalizeWorkspacePath(path);
    const index = await this.readIndex();
    const existed = index.delete(normalized);
    if (!existed) return false;
    await (await this.filesDirectory()).removeEntry?.(this.fileName(normalized));
    await this.writeIndex();
    return true;
  }
  async listFiles(prefix = "/") {
    const normalized = normalizeWorkspacePath(prefix);
    const files = Array.from((await this.readIndex()).values(), (file) => ({ ...file, kind: "file" }));
    const directFiles = files.filter((file) => {
      if (!pathWithinPrefix(file.path, normalized)) return false;
      const relative = normalized === "/" ? file.path.slice(1) : file.path.slice(normalized.length + 1);
      return relative.length > 0 && !relative.includes("/");
    });
    return [...directoryEntriesFor(files, normalized), ...directFiles].sort((a, b) => a.path.localeCompare(b.path));
  }
  async clear() {
    const paths = Array.from((await this.readIndex()).keys());
    for (const path of paths) await (await this.filesDirectory()).removeEntry?.(this.fileName(path));
    this.index = /* @__PURE__ */ new Map();
    await this.writeIndex();
  }
};
function requestResult(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error ?? new BrowserVfsError("indexeddb_error", "IndexedDB request failed"));
  });
}
function transactionDone(transaction) {
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve();
    transaction.onabort = () => reject(transaction.error ?? new BrowserVfsError("indexeddb_aborted", "IndexedDB transaction aborted"));
    transaction.onerror = () => reject(transaction.error ?? new BrowserVfsError("indexeddb_error", "IndexedDB transaction failed"));
  });
}
var IndexedDbWorkspaceBackend = class _IndexedDbWorkspaceBackend {
  constructor(db) {
    this.db = db;
  }
  db;
  static async create(options = {}) {
    const scope = options.globalScope ?? globalThis;
    const indexedDB = scope.indexedDB;
    if (typeof indexedDB?.open !== "function") throw new BrowserVfsError("indexeddb_unavailable", "IndexedDB is not available");
    const request = indexedDB.open("w3kits-browser-vfs:" + (options.namespace ?? "default"), 1);
    request.onupgradeneeded = () => {
      const db = request.result;
      if (!db.objectStoreNames.contains("files")) db.createObjectStore("files", { keyPath: "path" });
    };
    return new _IndexedDbWorkspaceBackend(await requestResult(request));
  }
  store(mode) {
    const transaction = this.db.transaction("files", mode);
    return { store: transaction.objectStore("files"), done: transactionDone(transaction) };
  }
  async stat(path) {
    const { store } = this.store("readonly");
    const record = await requestResult(store.get(normalizeWorkspacePath(path)));
    return record?.metadata ?? null;
  }
  async readFile(path) {
    const { store } = this.store("readonly");
    const record = await requestResult(store.get(normalizeWorkspacePath(path)));
    return record ? new Uint8Array(record.body) : null;
  }
  async writeFile(path, data, options = {}) {
    const normalized = normalizeWorkspacePath(path);
    if (normalized === "/") throw new BrowserVfsError("invalid_path", "Workspace file path must not be root");
    const body = new Uint8Array(data);
    const metadata = await fileStat(normalized, body, options);
    const { store, done } = this.store("readwrite");
    const buffer = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
    await requestResult(store.put({ path: normalized, body: buffer, metadata }));
    await done;
    return metadata;
  }
  async deleteFile(path) {
    const normalized = normalizeWorkspacePath(path);
    const existing = await this.stat(normalized);
    if (!existing) return false;
    const { store, done } = this.store("readwrite");
    await requestResult(store.delete(normalized));
    await done;
    return true;
  }
  async listFiles(prefix = "/") {
    const normalized = normalizeWorkspacePath(prefix);
    const { store } = this.store("readonly");
    const records = await requestResult(store.getAll());
    const files = records.map((record) => ({ ...record.metadata, kind: "file" }));
    const directFiles = files.filter((file) => {
      if (!pathWithinPrefix(file.path, normalized)) return false;
      const relative = normalized === "/" ? file.path.slice(1) : file.path.slice(normalized.length + 1);
      return relative.length > 0 && !relative.includes("/");
    });
    return [...directoryEntriesFor(files, normalized), ...directFiles].sort((a, b) => a.path.localeCompare(b.path));
  }
  async clear() {
    const { store, done } = this.store("readwrite");
    await requestResult(store.clear());
    await done;
  }
};
async function createPersistentBrowserWorkspaceBackend(options = {}) {
  if (isOpfsAvailable(options.globalScope ?? globalThis)) {
    try {
      return await OpfsWorkspaceBackend.create(options);
    } catch {
    }
  }
  if (isIndexedDbAvailable(options.globalScope ?? globalThis)) {
    try {
      return await IndexedDbWorkspaceBackend.create(options);
    } catch {
    }
  }
  return new MemoryWorkspaceBackend();
}
async function createPersistentBrowserWorkspace(options = {}) {
  return createBrowserWorkspace(await createPersistentBrowserWorkspaceBackend(options));
}
function createBrowserWorkspace(backend = new MemoryWorkspaceBackend()) {
  const dirty = /* @__PURE__ */ new Set();
  return {
    async readFile(path) {
      return backend.readFile(path);
    },
    async readText(path) {
      const bytes = await backend.readFile(path);
      return bytes ? TEXT_DECODER.decode(bytes) : null;
    },
    async writeFile(path, data, options) {
      const metadata = await backend.writeFile(path, toBytes(data), options);
      dirty.add(metadata.path);
      return metadata;
    },
    async deleteFile(path) {
      const normalized = normalizeWorkspacePath(path);
      const deleted = await backend.deleteFile(normalized);
      if (deleted) dirty.add(normalized);
      return deleted;
    },
    stat(path) {
      return backend.stat(path);
    },
    listFiles(prefix) {
      return backend.listFiles(prefix);
    },
    dirtyPaths() {
      return Array.from(dirty).sort();
    },
    markClean(paths) {
      if (!paths) {
        dirty.clear();
        return;
      }
      for (const path of paths) dirty.delete(normalizeWorkspacePath(path));
    },
    async exportSnapshot() {
      const entries = await backend.listFiles("/");
      const allFiles = await collectFilesRecursive(backend, entries);
      const files = [];
      for (const entry of allFiles) {
        const body = await backend.readFile(entry.path);
        if (!body) continue;
        files.push({
          path: entry.path,
          contentBase64: bytesToBase64(body),
          ...entry.contentType ? { contentType: entry.contentType } : {},
          updatedAt: entry.updatedAt
        });
      }
      return TEXT_ENCODER.encode(JSON.stringify({ version: 1, files }));
    },
    async importSnapshot(data, options = {}) {
      const json = typeof data === "string" ? data : TEXT_DECODER.decode(data);
      const snapshot = JSON.parse(json);
      if (snapshot.version !== 1 || !Array.isArray(snapshot.files)) throw new BrowserVfsError("invalid_snapshot", "Unsupported workspace snapshot");
      await backend.clear();
      dirty.clear();
      for (const file of snapshot.files) {
        const metadata = await backend.writeFile(file.path, base64ToBytes(file.contentBase64), file.contentType ? { contentType: file.contentType } : {});
        if (options.markDirty) dirty.add(metadata.path);
      }
    }
  };
}
async function collectFilesRecursive(backend, entries) {
  const files = [];
  for (const entry of entries) {
    if (entry.kind === "file") {
      files.push(entry);
      continue;
    }
    files.push(...await collectFilesRecursive(backend, await backend.listFiles(entry.path)));
  }
  return files.sort((a, b) => a.path.localeCompare(b.path));
}
function isOpfsAvailable(globalScope = globalThis) {
  return typeof globalScope.navigator?.storage?.getDirectory === "function";
}
function isIndexedDbAvailable(globalScope = globalThis) {
  return typeof globalScope.indexedDB?.open === "function";
}
export {
  BrowserVfsError,
  IndexedDbWorkspaceBackend,
  MemoryWorkspaceBackend,
  OpfsWorkspaceBackend,
  createBrowserWorkspace,
  createPersistentBrowserWorkspace,
  createPersistentBrowserWorkspaceBackend,
  isIndexedDbAvailable,
  isOpfsAvailable,
  normalizeWorkspacePath,
  projectWorkspaceRoot
};
