/**
 * LocalWorkspace — local filesystem implementation of the VL Workspace API
 *
 * Drop-in replacement for the remote Workspace REST API.
 * All operations are scoped under: <root>/<gid>/<path>
 *
 * Supported actions (mirrors remote API):
 *   File: createFile, listFile, readFile, writeFile, deleteFile
 *   Batch: createFiles, readFiles, writeFiles, deleteFiles
 *   Doc:  createDoc, listDoc, readDoc, writeDoc, deleteDoc
 *
 * Documents are stored as JSON files under <root>/<gid>/.docs/<path>.json
 * with metadata: { version, content, updatedAt }
 */
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';

export class LocalWorkspace {
  /**
   * @param {string} root - Workspace root directory (absolute path)
   */
  constructor(root) {
    this.root = path.resolve(root);
  }

  /**
   * Resolve and validate a scoped file path.
   * Ensures the resolved path stays within the workspace root.
   * @param {string} gid - Group/workspace ID
   * @param {string} filePath - Relative file path
   * @returns {string} Absolute resolved path
   * @throws {Error} If path traversal is detected
   */
  _resolve(gid, filePath) {
    const base = path.resolve(this.root, gid || 'default');
    const full = path.resolve(base, filePath);
    if (!full.startsWith(base + path.sep) && full !== base) {
      throw new Error(`Path traversal rejected: ${filePath}`);
    }
    return full;
  }

  /**
   * Resolve a doc storage path.
   * Docs are stored in <root>/<gid>/.docs/<path>.json
   * @param {string} gid - Group ID (or 'shared' for shared docs)
   * @param {string} docPath - Document path
   * @returns {string} Absolute path to doc JSON file
   */
  _resolveDoc(gid, docPath) {
    const base = path.resolve(this.root, gid || 'default', '.docs');
    // Normalize doc path: strip leading slashes, append .json if needed
    let normalized = (docPath || '').replace(/^\/+/, '');
    if (!normalized.endsWith('.json')) normalized += '.json';
    const full = path.resolve(base, normalized);
    if (!full.startsWith(base + path.sep) && full !== base) {
      throw new Error(`Doc path traversal rejected: ${docPath}`);
    }
    return full;
  }

  /**
   * Ensure parent directories exist for a file path.
   * @param {string} fullPath
   */
  async _ensureDir(fullPath) {
    await fs.mkdir(path.dirname(fullPath), { recursive: true });
  }

  // ────────────────────────────────────────────────────
  // File Operations
  // ────────────────────────────────────────────────────

  /**
   * Create a file (empty if no content provided).
   * @param {{ gid: string, path: string, content?: string }} params
   * @returns {{ gid: string, path: string, version: number }}
   */
  async createFile({ gid, path: filePath, content }) {
    const full = this._resolve(gid, filePath);
    await this._ensureDir(full);
    if (!fsSync.existsSync(full)) {
      await fs.writeFile(full, content || '', 'utf-8');
    }
    return { gid, path: filePath, version: 1 };
  }

  /**
   * List files in a workspace.
   * @param {{ gid: string, path?: string }} params
   * @returns {{ files: Array<{ gid: string, path: string, version: number }> }}
   */
  async listFile({ gid, path: dirPath }) {
    const base = this._resolve(gid, dirPath || '.');
    const files = [];

    const walk = async (dir, prefix) => {
      let entries;
      try {
        entries = await fs.readdir(dir, { withFileTypes: true });
      } catch {
        return; // Directory doesn't exist
      }
      for (const entry of entries) {
        // Skip hidden directories (.docs, .meta, etc.)
        if (entry.name.startsWith('.')) continue;

        const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
        if (entry.isDirectory()) {
          await walk(path.join(dir, entry.name), relPath);
        } else {
          files.push({ gid, path: relPath, version: 1 });
        }
      }
    };

    await walk(base, dirPath && dirPath !== '.' ? dirPath : '');
    return { files };
  }

  /**
   * Read a file.
   * @param {{ gid: string, path: string, version?: number }} params
   * @returns {{ version: number, status: number, content: string }}
   */
  async readFile({ gid, path: filePath }) {
    const full = this._resolve(gid, filePath);
    try {
      const content = await fs.readFile(full, 'utf-8');
      return { version: 1, status: 0, content };
    } catch (err) {
      if (err.code === 'ENOENT') {
        throw new Error(`File not found: ${filePath}`);
      }
      throw err;
    }
  }

  /**
   * Write content to a file (creates if not exists).
   * @param {{ gid: string, path: string, content: string }} params
   * @returns {{ gid: string, path: string, version: number }}
   */
  async writeFile({ gid, path: filePath, content }) {
    const full = this._resolve(gid, filePath);
    await this._ensureDir(full);
    await fs.writeFile(full, content || '', 'utf-8');
    return { gid, path: filePath, version: 1 };
  }

  /**
   * Delete a file.
   * @param {{ gid: string, path: string }} params
   */
  async deleteFile({ gid, path: filePath }) {
    const full = this._resolve(gid, filePath);
    try {
      await fs.unlink(full);
    } catch (err) {
      if (err.code !== 'ENOENT') throw err;
      // File already doesn't exist — idempotent
    }
    return { success: true };
  }

  // ────────────────────────────────────────────────────
  // Batch File Operations
  // ────────────────────────────────────────────────────

  /**
   * Create multiple files.
   * @param {{ gid: string, paths?: string[], files?: Array<{path: string, content?: string}> }} params
   */
  async createFiles({ gid, paths, files }) {
    const items = files || (paths || []).map((p) => ({ path: p }));
    await Promise.all(items.map((f) => this.createFile({ gid, path: f.path, content: f.content })));
    return { success: true };
  }

  /**
   * Read multiple files.
   * @param {{ gid: string, paths: string[] }} params
   * @returns {{ files: Array<{ gid: string, path: string, content: string }> }}
   */
  async readFiles({ gid, paths }) {
    const results = await Promise.all(
      (paths || []).map(async (p) => {
        try {
          const { content } = await this.readFile({ gid, path: p });
          return { gid, path: p, content };
        } catch {
          return { gid, path: p, content: null, error: 'Not found' };
        }
      })
    );
    return { files: results };
  }

  /**
   * Write multiple files.
   * @param {{ gid: string, files: Array<{ path: string, content: string }> }} params
   */
  async writeFiles({ gid, files }) {
    await Promise.all(
      (files || []).map((f) => this.writeFile({ gid, path: f.path, content: f.content }))
    );
    return { success: true };
  }

  /**
   * Delete multiple files.
   * @param {{ gid: string, paths: string[] }} params
   */
  async deleteFiles({ gid, paths }) {
    await Promise.all((paths || []).map((p) => this.deleteFile({ gid, path: p })));
    return { success: true };
  }

  // ────────────────────────────────────────────────────
  // Document Operations
  // ────────────────────────────────────────────────────

  /**
   * Create a document.
   * @param {{ path: string, content?: string, shared?: boolean }} params
   * @returns {{ path: string, version: number }}
   */
  async createDoc({ path: docPath, content, shared }) {
    const gid = shared ? '_shared' : '_docs';
    const full = this._resolveDoc(gid, docPath);
    await this._ensureDir(full);
    const doc = {
      version: 1,
      content: content || '',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };
    await fs.writeFile(full, JSON.stringify(doc, null, 2), 'utf-8');
    return { path: docPath, version: 1 };
  }

  /**
   * List documents.
   * @param {{ shared?: boolean }} params
   * @returns {{ docs: Array<{ path: string, version: number }> }}
   */
  async listDoc({ shared }) {
    const gid = shared ? '_shared' : '_docs';
    const docsDir = path.resolve(this.root, gid, '.docs');
    const docs = [];

    const walk = async (dir, prefix) => {
      let entries;
      try {
        entries = await fs.readdir(dir, { withFileTypes: true });
      } catch {
        return;
      }
      for (const entry of entries) {
        const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
        if (entry.isDirectory()) {
          await walk(path.join(dir, entry.name), relPath);
        } else if (entry.name.endsWith('.json')) {
          try {
            const raw = await fs.readFile(path.join(dir, entry.name), 'utf-8');
            const doc = JSON.parse(raw);
            docs.push({
              path: relPath.replace(/\.json$/, ''),
              version: doc.version || 1,
            });
          } catch {
            // Malformed doc file — skip
          }
        }
      }
    };

    await walk(docsDir, '');
    return { docs };
  }

  /**
   * Read a document.
   * @param {{ path: string, version?: number, shared?: boolean }} params
   * @returns {{ version: number, content: string }}
   */
  async readDoc({ path: docPath, shared }) {
    const gid = shared ? '_shared' : '_docs';
    const full = this._resolveDoc(gid, docPath);
    try {
      const raw = await fs.readFile(full, 'utf-8');
      const doc = JSON.parse(raw);
      return { version: doc.version || 1, content: doc.content || '' };
    } catch (err) {
      if (err.code === 'ENOENT') {
        throw new Error(`Document not found: ${docPath}`);
      }
      throw err;
    }
  }

  /**
   * Write a document (auto-increment version).
   * @param {{ path: string, content: string, shared?: boolean }} params
   * @returns {{ path: string, version: number }}
   */
  async writeDoc({ path: docPath, content, shared }) {
    const gid = shared ? '_shared' : '_docs';
    const full = this._resolveDoc(gid, docPath);
    await this._ensureDir(full);

    let version = 1;
    try {
      const raw = await fs.readFile(full, 'utf-8');
      const existing = JSON.parse(raw);
      version = (existing.version || 0) + 1;
    } catch {
      // New doc
    }

    const doc = {
      version,
      content: content || '',
      updatedAt: new Date().toISOString(),
    };
    await fs.writeFile(full, JSON.stringify(doc, null, 2), 'utf-8');
    return { path: docPath, version };
  }

  /**
   * Delete a document.
   * @param {{ path: string, shared?: boolean }} params
   */
  async deleteDoc({ path: docPath, shared }) {
    const gid = shared ? '_shared' : '_docs';
    const full = this._resolveDoc(gid, docPath);
    try {
      await fs.unlink(full);
    } catch (err) {
      if (err.code !== 'ENOENT') throw err;
    }
    return { success: true };
  }
}
