import type {
  IFileSystem,
  IReactiveFileSystem,
  FileEntry,
  FileStat,
  FileMetadata,
  FSEvent,
  Disposable,
  FileSystemEventPayloads,
  IFileSystemCapabilities,
} from "@firesystem/core";
import {
  normalizePath,
  dirname,
  basename,
  join,
  TypedEventEmitter,
  FileSystemEvents,
  BaseFileSystem,
} from "@firesystem/core";
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
  ListObjectsV2Command,
  HeadObjectCommand,
  CopyObjectCommand,
  DeleteObjectsCommand,
  type _Object,
} from "@aws-sdk/client-s3";
import type { S3FileSystemConfig } from "./types";

interface WatchListener {
  id: string;
  pattern: string;
  callback: (event: FSEvent) => void;
}

/**
 * S3FileSystem - Adaptação do Amazon S3 para interface de FileSystem
 *
 * CARACTERÍSTICAS DO S3:
 * - Object storage, não file system hierárquico tradicional
 * - Não tem diretórios reais, apenas prefixos (keys com "/")
 * - Todas as operações são eventuais (eventual consistency)
 * - Não tem rename atômico - sempre copy + delete
 * - Permissões via IAM, bucket policies e ACLs (não Unix-style)
 *
 * MODOS DE OPERAÇÃO:
 * - "strict": Simula diretórios com markers vazios (compatível com ferramentas)
 * - "lenient": Diretórios são virtuais, existem apenas quando têm conteúdo
 *
 * LIMITAÇÕES:
 * - rename de diretórios é caro (copia todos os objetos)
 * - watch é simulado (não há notificações nativas)
 * - Permissões só podem ser verificadas parcialmente
 * - Tamanho máximo: 5GB (single upload) ou 5TB (multipart)
 *
 * CONSIDERAÇÕES DE PERFORMANCE:
 * - Liste apenas o necessário (ListObjectsV2 é paginado)
 * - Use prefixos para limitar buscas
 * - Evite rename de diretórios grandes
 * - Cache pode ajudar, mas cuidado com consistência
 */
export class S3FileSystem extends BaseFileSystem {
  private client: S3Client;
  private config: S3FileSystemConfig & {
    prefix: string;
    mode: "strict" | "lenient";
  };
  private watchers: WatchListener[] = [];
  public readonly events = new TypedEventEmitter<FileSystemEventPayloads>();

  // Capabilities do S3 - Reflete as características reais do S3
  readonly capabilities: IFileSystemCapabilities = {
    // Operações básicas
    readonly: false, // Depende das permissões IAM/bucket policy
    caseSensitive: true, // S3 keys são case-sensitive
    atomicRename: false, // S3 não tem rename - é sempre copy + delete

    // Limites
    maxFileSize: 5 * 1024 * 1024 * 1024 * 1024, // 5TB com multipart upload
    maxPathLength: 1024, // S3 key length limit

    // Features básicas
    supportsWatch: false, // S3 não tem watch nativo (apenas via polling)
    supportsMetadata: true, // S3 suporta user metadata
    supportsGlob: false, // S3 não tem glob nativo (implementado via list + filter)

    // Features avançadas - específicas do S3
    supportsTrueDirectories: false, // S3 usa apenas prefixos, não diretórios reais
    supportsAtomicOperations: false, // Nenhuma operação é verdadeiramente atômica
    supportsMultipartUpload: true, // Para arquivos > 5GB
    supportsPermissions: false, // Sem permissões no estilo Unix
    metadataLimit: 2048, // 2KB total para user metadata
    description: "S3-compatible object storage with eventual consistency",
  };

  constructor(config: S3FileSystemConfig) {
    super();
    this.config = {
      ...config,
      prefix: config.prefix || "/",
      mode: config.mode || "strict",
    };

    // Normalize prefix
    if (this.config.prefix !== "/") {
      this.config.prefix = normalizePath(this.config.prefix);
      if (!this.config.prefix.endsWith("/")) {
        this.config.prefix += "/";
      }
      // Remove leading slash for S3
      this.config.prefix = this.config.prefix.substring(1);
    }

    this.client = new S3Client({
      region: config.region,
      credentials: config.credentials,
      ...config.clientOptions,
    });
  }

  async initialize(): Promise<void> {
    const startTime = Date.now();
    this.events.emit(FileSystemEvents.INITIALIZING, undefined as any);

    try {
      // Only create root directory marker in strict mode
      if (
        this.config.mode === "strict" &&
        this.config.prefix !== "/" &&
        this.config.prefix !== ""
      ) {
        await this.ensureDirectoryMarker(this.config.prefix);
      }

      this.events.emit(FileSystemEvents.INITIALIZED, {
        duration: Date.now() - startTime,
      });
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "initialize",
        path: "/",
        error: error as Error,
      });
      throw error;
    }
  }

  private toS3Key(path: string): string {
    const normalized = normalizePath(path);
    if (normalized === "/") {
      return this.config.prefix === "/" ? "" : this.config.prefix;
    }

    const withoutLeadingSlash = normalized.substring(1);
    if (this.config.prefix === "/" || this.config.prefix === "") {
      return withoutLeadingSlash;
    }

    return this.config.prefix + withoutLeadingSlash;
  }

  private fromS3Key(key: string): string {
    if (this.config.prefix === "/" || this.config.prefix === "") {
      return "/" + key;
    }

    if (key.startsWith(this.config.prefix)) {
      const withoutPrefix = key.substring(this.config.prefix.length);
      return "/" + withoutPrefix;
    }

    return "/" + key;
  }

  private async streamToString(stream: any): Promise<string> {
    const chunks: any[] = [];
    for await (const chunk of stream) {
      chunks.push(chunk);
    }
    return Buffer.concat(chunks).toString("utf-8");
  }

  private async ensureDirectoryMarker(s3Key: string): Promise<void> {
    const key = s3Key.endsWith("/") ? s3Key : s3Key + "/";

    try {
      await this.client.send(
        new PutObjectCommand({
          Bucket: this.config.bucket,
          Key: key,
          Body: "",
          ContentType: "application/x-directory",
          Metadata: {
            "x-amz-meta-type": "directory",
            "x-amz-meta-created": new Date().toISOString(),
          },
        }),
      );
    } catch (error) {
      throw new Error(`Failed to create directory marker: ${error}`);
    }
  }

  private async ensureParentExists(path: string): Promise<void> {
    // Only ensure parent exists in strict mode
    if (this.config.mode !== "strict") return;

    const parent = dirname(path);
    if (parent === "/" || parent === ".") return;

    const s3Key = this.toS3Key(parent);
    await this.ensureDirectoryMarker(s3Key);
  }

  private parseS3Metadata(metadata?: Record<string, string>): {
    created?: Date;
    modified?: Date;
    fileMetadata?: FileMetadata;
    type?: "file" | "directory";
  } {
    const result: any = {};

    if (metadata) {
      if (metadata["x-amz-meta-created"]) {
        result.created = new Date(metadata["x-amz-meta-created"]);
      }
      if (metadata["x-amz-meta-modified"]) {
        result.modified = new Date(metadata["x-amz-meta-modified"]);
      }
      if (metadata["x-amz-meta-type"]) {
        result.type = metadata["x-amz-meta-type"] as "file" | "directory";
      }
      if (metadata["x-amz-meta-custom"]) {
        try {
          result.fileMetadata = JSON.parse(metadata["x-amz-meta-custom"]);
        } catch {
          // Ignore parse errors
        }
      }
    }

    return result;
  }

  private createS3Metadata(
    type: "file" | "directory",
    metadata?: FileMetadata,
    created?: Date,
  ): Record<string, string> {
    const now = new Date();
    const s3Metadata: Record<string, string> = {
      "x-amz-meta-type": type,
      "x-amz-meta-created": (created || now).toISOString(),
      "x-amz-meta-modified": now.toISOString(),
    };

    if (metadata) {
      s3Metadata["x-amz-meta-custom"] = JSON.stringify(metadata);
    }

    return s3Metadata;
  }

  async readFile(path: string): Promise<FileEntry> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `read-${normalized}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "readFile",
      path: normalized,
      id: operationId,
    });

    try {
      const s3Key = this.toS3Key(normalized);

      // First check if it's a directory (only if path doesn't already end with /)
      if (!s3Key.endsWith("/")) {
        const dirKey = s3Key + "/";
        try {
          const dirCheck = await this.client.send(
            new HeadObjectCommand({
              Bucket: this.config.bucket,
              Key: dirKey,
            }),
          );
          // If we get here, it means the directory marker exists
          if (
            dirCheck &&
            dirCheck.Metadata?.["x-amz-meta-type"] === "directory"
          ) {
            throw new Error(
              `EISDIR: illegal operation on a directory, read '${normalized}'`,
            );
          }
        } catch (error: any) {
          // Ignore not found errors - just means it's not a directory
          if (
            error.name !== "NoSuchKey" &&
            error.name !== "NotFound" &&
            !error.message?.includes("EISDIR")
          ) {
            // For other errors, don't throw - just continue trying to read as file
          }
          if (error.message?.includes("EISDIR")) {
            throw error;
          }
        }
      }

      const response = await this.client.send(
        new GetObjectCommand({
          Bucket: this.config.bucket,
          Key: s3Key,
        }),
      );

      if (!response.Body) {
        throw new Error(`File not found: ${normalized}`);
      }

      const parsedMetadata = this.parseS3Metadata(response.Metadata);

      // In lenient mode, infer type from key
      let type = parsedMetadata.type;
      if (!type && this.config.mode === "lenient") {
        type = s3Key.endsWith("/") ? "directory" : "file";
      }

      // Check if trying to read a directory
      if (type === "directory" || s3Key.endsWith("/")) {
        throw new Error(
          `EISDIR: illegal operation on a directory, read '${normalized}'`,
        );
      }

      // Read content based on metadata
      let content: any;
      if (parsedMetadata.fileMetadata?.isBinary) {
        // Decode from base64 and return as ArrayBuffer
        const base64Content = await this.streamToString(response.Body);
        const buffer = Buffer.from(base64Content, "base64");
        content = buffer.buffer.slice(
          buffer.byteOffset,
          buffer.byteOffset + buffer.byteLength,
        );
      } else {
        content = await this.streamToString(response.Body);

        // Try to parse JSON if it looks like JSON
        // This handles objects that were stringified during write
        if (
          content &&
          typeof content === "string" &&
          (content.startsWith("{") || content.startsWith("["))
        ) {
          try {
            content = JSON.parse(content);
          } catch {
            // If parse fails, keep as string
          }
        }
      }

      const result: FileEntry = {
        path: normalized,
        name: basename(normalized),
        type: type || "file",
        size: response.ContentLength || 0,
        created:
          parsedMetadata.created ||
          new Date(response.LastModified || Date.now()),
        modified:
          parsedMetadata.modified ||
          new Date(response.LastModified || Date.now()),
        metadata: parsedMetadata.fileMetadata,
        content: content,
      };

      this.events.emit(FileSystemEvents.FILE_READ, {
        path: normalized,
        size: result.size || 0,
      });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "readFile",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });

      return result;
    } catch (error: any) {
      if (error.name === "NoSuchKey") {
        const notFoundError = new Error(
          `ENOENT: no such file or directory, open '${normalized}'`,
        );
        this.events.emit(FileSystemEvents.OPERATION_ERROR, {
          operation: "readFile",
          path: normalized,
          error: notFoundError,
        });
        throw notFoundError;
      }

      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "readFile",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async writeFile(
    path: string,
    content: any,
    metadata?: FileMetadata,
  ): Promise<FileEntry> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `write-${normalized}-${startTime}`;
    let contentStr: string;
    let isBinary = false;
    let originalSize: number;

    if (content === null || content === undefined) {
      contentStr = "";
      originalSize = 0;
    } else if (typeof content === "string") {
      contentStr = content;
      originalSize = Buffer.byteLength(contentStr);
    } else if (content instanceof ArrayBuffer) {
      contentStr = Buffer.from(content).toString("base64");
      isBinary = true;
      originalSize = content.byteLength; // Use original ArrayBuffer size
    } else {
      contentStr = JSON.stringify(content);
      originalSize = Buffer.byteLength(contentStr);
    }

    const size = originalSize;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "writeFile",
      path: normalized,
      id: operationId,
    });
    this.events.emit(FileSystemEvents.FILE_WRITING, { path: normalized, size });

    try {
      // In strict mode, check if parent exists
      if (this.config.mode === "strict") {
        const parent = dirname(normalized);
        if (parent !== "/" && parent !== ".") {
          const parentExists = await this.exists(parent);
          if (!parentExists) {
            throw new Error(
              `ENOENT: no such file or directory, open '${normalized}'`,
            );
          }
        }
      }

      await this.ensureParentExists(normalized);

      const s3Key = this.toS3Key(normalized);

      // Check if file already exists to preserve created date
      let existingCreated: Date | undefined;
      try {
        const existing = await this.client.send(
          new HeadObjectCommand({
            Bucket: this.config.bucket,
            Key: s3Key,
          }),
        );
        if (existing.Metadata?.["x-amz-meta-created"]) {
          existingCreated = new Date(existing.Metadata["x-amz-meta-created"]);
        }
      } catch {
        // File doesn't exist, which is fine
      }

      const finalMetadata = isBinary
        ? { ...metadata, isBinary: true }
        : metadata;
      const s3Metadata = this.createS3Metadata(
        "file",
        finalMetadata,
        existingCreated,
      );

      await this.client.send(
        new PutObjectCommand({
          Bucket: this.config.bucket,
          Key: s3Key,
          Body: contentStr,
          ContentType: "text/plain",
          Metadata: s3Metadata,
        }),
      );

      const now = new Date();
      const result: FileEntry = {
        path: normalized,
        name: basename(normalized),
        type: "file",
        size,
        created: existingCreated || new Date(s3Metadata["x-amz-meta-created"]),
        modified: now,
        metadata: finalMetadata,
        content,
      };

      // Notify watchers
      this.notifyWatchers({
        type: existingCreated ? "updated" : "created",
        path: normalized,
        timestamp: now,
      });

      this.events.emit(FileSystemEvents.FILE_WRITTEN, {
        path: normalized,
        size,
      });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "writeFile",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });

      return result;
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "writeFile",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async deleteFile(path: string): Promise<void> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `delete-${normalized}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "deleteFile",
      path: normalized,
      id: operationId,
    });
    this.events.emit(FileSystemEvents.FILE_DELETING, { path: normalized });

    try {
      // Check if file exists first
      const exists = await this.exists(normalized);
      if (!exists) {
        throw new Error(
          `ENOENT: no such file or directory, unlink '${normalized}'`,
        );
      }

      const s3Key = this.toS3Key(normalized);

      // Check if it's a directory with contents
      if (await this.isDirectory(normalized)) {
        const entries = await this.readDir(normalized);
        if (entries.length > 0) {
          throw new Error(
            `ENOTEMPTY: directory not empty, rmdir '${normalized}'`,
          );
        }
      }

      await this.client.send(
        new DeleteObjectCommand({
          Bucket: this.config.bucket,
          Key: s3Key,
        }),
      );

      // Notify watchers
      this.notifyWatchers({
        type: "deleted",
        path: normalized,
        timestamp: new Date(),
      });

      this.events.emit(FileSystemEvents.FILE_DELETED, { path: normalized });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "deleteFile",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "deleteFile",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async exists(path: string): Promise<boolean> {
    const normalized = normalizePath(path);

    try {
      const s3Key = this.toS3Key(normalized);

      // Check as file first
      try {
        await this.client.send(
          new HeadObjectCommand({
            Bucket: this.config.bucket,
            Key: s3Key,
          }),
        );
        return true;
      } catch {
        // In lenient mode, also check if it's a prefix with contents
        if (this.config.mode === "lenient") {
          const prefix = s3Key.endsWith("/") ? s3Key : s3Key + "/";
          const response = await this.client.send(
            new ListObjectsV2Command({
              Bucket: this.config.bucket,
              Prefix: prefix,
              MaxKeys: 1,
            }),
          );
          return (response.Contents?.length || 0) > 0;
        }

        // In strict mode, check for directory marker
        const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/";
        try {
          await this.client.send(
            new HeadObjectCommand({
              Bucket: this.config.bucket,
              Key: dirKey,
            }),
          );
          return true;
        } catch {
          return false;
        }
      }
    } catch {
      return false;
    }
  }

  async readDir(path: string): Promise<FileEntry[]> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `readdir-${normalized}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "readDir",
      path: normalized,
      id: operationId,
    });

    try {
      // Check if directory exists first (except root)
      if (normalized !== "/") {
        const exists = await this.exists(normalized);
        if (!exists) {
          throw new Error(
            `ENOENT: no such file or directory, scandir '${normalized}'`,
          );
        }
      }

      const s3Prefix = this.toS3Key(normalized);
      const prefix =
        s3Prefix === ""
          ? ""
          : s3Prefix.endsWith("/")
            ? s3Prefix
            : s3Prefix + "/";

      const entries: FileEntry[] = [];
      const seenPaths = new Set<string>();
      let continuationToken: string | undefined;

      do {
        const response = await this.client.send(
          new ListObjectsV2Command({
            Bucket: this.config.bucket,
            Prefix: prefix,
            Delimiter: "/",
            ContinuationToken: continuationToken,
          }),
        );

        // Add directories (CommonPrefixes)
        if (response.CommonPrefixes) {
          for (const commonPrefix of response.CommonPrefixes) {
            if (commonPrefix.Prefix) {
              const path = this.fromS3Key(commonPrefix.Prefix);
              if (!seenPaths.has(path)) {
                seenPaths.add(path);
                entries.push({
                  path,
                  name: basename(path.endsWith("/") ? path.slice(0, -1) : path),
                  type: "directory",
                  size: 0,
                  created: new Date(),
                  modified: new Date(),
                });
              }
            }
          }
        }

        // Add files (Contents)
        if (response.Contents) {
          for (const object of response.Contents) {
            if (object.Key) {
              // Skip directory markers
              if (object.Key.endsWith("/")) continue;

              // Skip objects not directly in this directory
              const relativePath = object.Key.substring(prefix.length);
              if (relativePath.includes("/")) continue;

              const path = this.fromS3Key(object.Key);
              if (!seenPaths.has(path)) {
                seenPaths.add(path);
                entries.push({
                  path,
                  name: basename(path),
                  type: "file",
                  size: object.Size || 0,
                  created: object.LastModified || new Date(),
                  modified: object.LastModified || new Date(),
                });
              }
            }
          }
        }

        continuationToken = response.NextContinuationToken;
      } while (continuationToken);

      this.events.emit(FileSystemEvents.DIR_READ, {
        path: normalized,
        count: entries.length,
      });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "readDir",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });

      return entries;
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "readDir",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async mkdir(path: string, recursive?: boolean): Promise<FileEntry> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `mkdir-${normalized}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "mkdir",
      path: normalized,
      id: operationId,
    });
    this.events.emit(FileSystemEvents.DIR_CREATING, {
      path: normalized,
      recursive: !!recursive,
    });

    try {
      // Check if trying to create root
      if (normalized === "/") {
        throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`);
      }

      // Check if directory already exists
      if (await this.exists(normalized)) {
        throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`);
      }

      // In lenient mode, directories are virtual - just return success
      if (this.config.mode === "lenient") {
        const now = new Date();
        const result: FileEntry = {
          path: normalized,
          name: basename(normalized),
          type: "directory",
          size: 0,
          created: now,
          modified: now,
        };

        this.notifyWatchers({
          type: "created",
          path: normalized,
          timestamp: now,
        });

        this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized });
        this.events.emit(FileSystemEvents.OPERATION_END, {
          operation: "mkdir",
          path: normalized,
          id: operationId,
          duration: Date.now() - startTime,
        });

        return result;
      }

      // Strict mode - create directory markers
      if (recursive) {
        // Create all parent directories
        const parts = normalized.split("/").filter(Boolean);
        let currentPath = "";

        for (const part of parts) {
          currentPath += "/" + part;
          const s3Key = this.toS3Key(currentPath);
          await this.ensureDirectoryMarker(s3Key);
        }
      } else {
        // Check if parent exists
        const parent = dirname(normalized);
        if (parent !== "/" && parent !== ".") {
          const parentExists = await this.exists(parent);
          if (!parentExists) {
            throw new Error(
              `ENOENT: no such file or directory, mkdir '${normalized}'`,
            );
          }
        }

        // Ensure parent exists
        await this.ensureParentExists(normalized);

        // Create directory marker
        const s3Key = this.toS3Key(normalized);
        await this.ensureDirectoryMarker(s3Key);
      }

      const now = new Date();
      const result: FileEntry = {
        path: normalized,
        name: basename(normalized),
        type: "directory",
        size: 0,
        created: now,
        modified: now,
      };

      // Notify watchers
      this.notifyWatchers({
        type: "created",
        path: normalized,
        timestamp: now,
      });

      this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "mkdir",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });

      return result;
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "mkdir",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async rmdir(path: string, recursive?: boolean): Promise<void> {
    const normalized = normalizePath(path);
    const startTime = Date.now();
    const operationId = `rmdir-${normalized}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "rmdir",
      path: normalized,
      id: operationId,
    });
    this.events.emit(FileSystemEvents.DIR_DELETING, {
      path: normalized,
      recursive: !!recursive,
    });

    try {
      // Check if trying to remove root
      if (normalized === "/") {
        throw new Error(
          `EBUSY: resource busy or locked, rmdir '${normalized}'`,
        );
      }

      // Check if directory exists
      const exists = await this.exists(normalized);
      if (!exists) {
        throw new Error(
          `ENOENT: no such file or directory, rmdir '${normalized}'`,
        );
      }

      // Check if it's actually a file
      if (!(await this.isDirectory(normalized))) {
        throw new Error(`ENOTDIR: not a directory, rmdir '${normalized}'`);
      }

      const s3Prefix = this.toS3Key(normalized);
      const prefix = s3Prefix.endsWith("/") ? s3Prefix : s3Prefix + "/";

      if (recursive) {
        // Delete all objects with this prefix
        const objectsToDelete: { Key: string }[] = [];
        let continuationToken: string | undefined;

        do {
          const response = await this.client.send(
            new ListObjectsV2Command({
              Bucket: this.config.bucket,
              Prefix: prefix,
              ContinuationToken: continuationToken,
            }),
          );

          if (response.Contents) {
            for (const object of response.Contents) {
              if (object.Key) {
                objectsToDelete.push({ Key: object.Key });
              }
            }
          }

          continuationToken = response.NextContinuationToken;
        } while (continuationToken);

        // Delete in batches (S3 allows max 1000 per request)
        const batchSize = 1000;
        for (let i = 0; i < objectsToDelete.length; i += batchSize) {
          const batch = objectsToDelete.slice(i, i + batchSize);
          await this.client.send(
            new DeleteObjectsCommand({
              Bucket: this.config.bucket,
              Delete: { Objects: batch },
            }),
          );
        }
      } else {
        // Check if directory is empty
        const response = await this.client.send(
          new ListObjectsV2Command({
            Bucket: this.config.bucket,
            Prefix: prefix,
            MaxKeys: 2, // We only need to know if there's more than the dir marker
          }),
        );

        const hasContents =
          (response.Contents?.length || 0) > 1 ||
          (response.Contents?.length === 1 &&
            !response.Contents[0].Key?.endsWith("/"));

        if (hasContents) {
          throw new Error(
            `ENOTEMPTY: directory not empty, rmdir '${normalized}'`,
          );
        }

        // Delete directory marker
        await this.client.send(
          new DeleteObjectCommand({
            Bucket: this.config.bucket,
            Key: prefix,
          }),
        );
      }

      // Notify watchers
      this.notifyWatchers({
        type: "deleted",
        path: normalized,
        timestamp: new Date(),
      });

      this.events.emit(FileSystemEvents.DIR_DELETED, { path: normalized });
      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "rmdir",
        path: normalized,
        id: operationId,
        duration: Date.now() - startTime,
      });
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "rmdir",
        path: normalized,
        error: error as Error,
      });
      throw error;
    }
  }

  async rename(oldPath: string, newPath: string): Promise<FileEntry> {
    const normalizedOld = normalizePath(oldPath);
    const normalizedNew = normalizePath(newPath);
    const startTime = Date.now();
    const operationId = `rename-${normalizedOld}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "rename",
      path: normalizedOld,
      id: operationId,
    });

    try {
      // Check if source exists
      if (!(await this.exists(normalizedOld))) {
        throw new Error(
          `ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`,
        );
      }

      // Check if target already exists
      if (await this.exists(normalizedNew)) {
        throw new Error(
          `EEXIST: file already exists, rename '${normalizedOld}' -> '${normalizedNew}'`,
        );
      }

      // Check if new parent exists
      const newParent = dirname(normalizedNew);
      if (
        newParent !== "/" &&
        newParent !== "." &&
        !(await this.exists(newParent))
      ) {
        throw new Error(
          `ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`,
        );
      }

      // Determine if source is a directory
      const isDirectory = await this.isDirectory(normalizedOld);

      if (!isDirectory) {
        // For files: S3 doesn't have rename, so we copy + delete
        // This is NOT atomic - the file exists in both places temporarily
        const oldEntry = await this.readFile(normalizedOld);
        await this.writeFile(
          normalizedNew,
          oldEntry.content,
          oldEntry.metadata,
        );
        await this.deleteFile(normalizedOld);

        const result = await this.readFile(normalizedNew);

        this.events.emit(FileSystemEvents.OPERATION_END, {
          operation: "rename",
          path: normalizedOld,
          id: operationId,
          duration: Date.now() - startTime,
        });

        return result;
      } else {
        // For directories: This is expensive in S3!
        // We need to list all objects with the old prefix and copy them one by one
        const s3OldPrefix = this.toS3Key(normalizedOld);
        const s3NewPrefix = this.toS3Key(normalizedNew);
        const oldPrefix = s3OldPrefix.endsWith("/")
          ? s3OldPrefix
          : s3OldPrefix + "/";
        const newPrefix = s3NewPrefix.endsWith("/")
          ? s3NewPrefix
          : s3NewPrefix + "/";

        // List all objects with old prefix
        const objectsToRename: string[] = [];
        let continuationToken: string | undefined;

        do {
          const response = await this.client.send(
            new ListObjectsV2Command({
              Bucket: this.config.bucket,
              Prefix: oldPrefix,
              ContinuationToken: continuationToken,
            }),
          );

          if (response.Contents) {
            for (const object of response.Contents) {
              if (object.Key) {
                objectsToRename.push(object.Key);
              }
            }
          }

          continuationToken = response.NextContinuationToken;
        } while (continuationToken);

        // Copy each object to new location
        for (const oldKey of objectsToRename) {
          const newKey = oldKey.replace(oldPrefix, newPrefix);
          await this.client.send(
            new CopyObjectCommand({
              Bucket: this.config.bucket,
              CopySource: `${this.config.bucket}/${oldKey}`,
              Key: newKey,
              MetadataDirective: "COPY",
            }),
          );
        }

        // Delete old objects
        if (objectsToRename.length > 0) {
          const batchSize = 1000;
          for (let i = 0; i < objectsToRename.length; i += batchSize) {
            const batch = objectsToRename.slice(i, i + batchSize);
            await this.client.send(
              new DeleteObjectsCommand({
                Bucket: this.config.bucket,
                Delete: { Objects: batch.map((Key) => ({ Key })) },
              }),
            );
          }
        }

        // Create directory marker for new directory if in strict mode
        if (this.config.mode === "strict") {
          await this.ensureDirectoryMarker(newPrefix);
        }

        // Return directory entry
        const result: FileEntry = {
          path: normalizedNew,
          name: basename(normalizedNew),
          type: "directory",
          size: 0,
          created: new Date(),
          modified: new Date(),
        };

        this.events.emit(FileSystemEvents.OPERATION_END, {
          operation: "rename",
          path: normalizedOld,
          id: operationId,
          duration: Date.now() - startTime,
        });

        return result;
      }
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "rename",
        path: normalizedOld,
        error: error as Error,
      });
      throw error;
    }
  }

  async move(sourcePaths: string[], targetPath: string): Promise<void> {
    const normalizedTarget = normalizePath(targetPath);
    const startTime = Date.now();
    const operationId = `move-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "move",
      path: sourcePaths[0],
      id: operationId,
    });

    try {
      // Check if target exists and is a directory
      if (!(await this.exists(normalizedTarget))) {
        throw new Error(
          `ENOENT: no such file or directory, open '${normalizedTarget}'`,
        );
      }

      if (!(await this.isDirectory(normalizedTarget))) {
        throw new Error(`ENOTDIR: not a directory, open '${normalizedTarget}'`);
      }

      for (const sourcePath of sourcePaths) {
        const normalizedSource = normalizePath(sourcePath);
        const targetFilePath = join(
          normalizedTarget,
          basename(normalizedSource),
        );

        // Read source
        const sourceEntry = await this.readFile(normalizedSource);

        // Write to target
        await this.writeFile(
          targetFilePath,
          sourceEntry.content,
          sourceEntry.metadata,
        );

        // Delete source
        await this.deleteFile(normalizedSource);
      }

      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "move",
        path: sourcePaths[0],
        id: operationId,
        duration: Date.now() - startTime,
      });
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "move",
        path: sourcePaths[0],
        error: error as Error,
      });
      throw error;
    }
  }

  async copy(sourcePath: string, targetPath: string): Promise<FileEntry> {
    const normalizedSource = normalizePath(sourcePath);
    const normalizedTarget = normalizePath(targetPath);
    const startTime = Date.now();
    const operationId = `copy-${normalizedSource}-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "copy",
      path: normalizedSource,
      id: operationId,
    });

    try {
      // Check if source is a directory
      if (await this.isDirectory(normalizedSource)) {
        throw new Error(
          `EISDIR: illegal operation on a directory, open '${normalizedSource}'`,
        );
      }

      // Read source
      const sourceEntry = await this.readFile(normalizedSource);

      // Write to target
      const result = await this.writeFile(
        normalizedTarget,
        sourceEntry.content,
        sourceEntry.metadata,
      );

      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "copy",
        path: normalizedSource,
        id: operationId,
        duration: Date.now() - startTime,
      });

      return result;
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "copy",
        path: normalizedSource,
        error: error as Error,
      });
      throw error;
    }
  }

  watch(pattern: string, callback: (event: FSEvent) => void): Disposable {
    const id = Math.random().toString(36).substr(2, 9);
    const listener: WatchListener = { id, pattern, callback };
    this.watchers.push(listener);

    return {
      dispose: () => {
        const index = this.watchers.findIndex((w) => w.id === id);
        if (index !== -1) {
          this.watchers.splice(index, 1);
        }
      },
    };
  }

  private notifyWatchers(event: FSEvent): void {
    for (const watcher of this.watchers) {
      // Simple pattern matching (could be enhanced with glob)
      if (
        watcher.pattern === "*" ||
        event.path.includes(watcher.pattern) ||
        this.matchGlob(event.path, watcher.pattern)
      ) {
        watcher.callback(event);
      }
    }
  }

  private matchGlob(path: string, pattern: string): boolean {
    // Enhanced glob matching
    if (pattern === "*") {
      // Single * matches files and directories in root
      return path.split("/").length === 2;
    }

    if (pattern === "**") {
      // ** matches everything
      return true;
    }

    // Convert glob to regex
    let regex = pattern
      .replace(/\*\*/g, "___DOUBLE_STAR___")
      .replace(/\*/g, "[^/]*")
      .replace(/___DOUBLE_STAR___/g, ".*")
      .replace(/\?/g, ".")
      .replace(/\//g, "\\/");

    return new RegExp("^" + regex + "$").test(path);
  }

  private async isDirectory(path: string): Promise<boolean> {
    const normalized = normalizePath(path);
    const s3Key = this.toS3Key(normalized);
    const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/";

    try {
      const response = await this.client.send(
        new HeadObjectCommand({
          Bucket: this.config.bucket,
          Key: dirKey,
        }),
      );
      return response.Metadata?.["x-amz-meta-type"] === "directory";
    } catch {
      // Check if any objects exist with this prefix
      const response = await this.client.send(
        new ListObjectsV2Command({
          Bucket: this.config.bucket,
          Prefix: dirKey,
          MaxKeys: 1,
        }),
      );
      return (response.Contents?.length || 0) > 0;
    }
  }

  async stat(path: string): Promise<FileStat> {
    const normalized = normalizePath(path);

    try {
      // Check if it's a directory first
      if (await this.isDirectory(normalized)) {
        const s3Key = this.toS3Key(normalized);
        const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/";

        try {
          const response = await this.client.send(
            new HeadObjectCommand({
              Bucket: this.config.bucket,
              Key: dirKey,
            }),
          );

          const metadata = this.parseS3Metadata(response.Metadata);
          return {
            path: normalized,
            size: 0,
            type: "directory",
            created:
              metadata.created || new Date(response.LastModified || Date.now()),
            modified:
              metadata.modified ||
              new Date(response.LastModified || Date.now()),
            readonly: false, // TODO: Check S3 bucket/object permissions
          };
        } catch {
          // Directory exists but no marker
          return {
            path: normalized,
            size: 0,
            type: "directory",
            created: new Date(),
            modified: new Date(),
            readonly: false, // TODO: Check S3 bucket/object permissions
          };
        }
      }

      // It's a file
      const entry = await this.readFile(normalized);

      return {
        path: entry.path,
        size: entry.size || 0,
        type: entry.type,
        created: entry.created || new Date(),
        modified: entry.modified || new Date(),
        readonly: false, // TODO: Check S3 bucket/object permissions
      };
    } catch (error: any) {
      if (error.message?.includes("EISDIR")) {
        // Handle the case where readFile throws EISDIR
        return {
          path: normalized,
          size: 0,
          type: "directory",
          created: new Date(),
          modified: new Date(),
          readonly: false, // TODO: Check S3 bucket/object permissions
        };
      }
      throw new Error(`Failed to stat ${normalized}: ${error}`);
    }
  }

  async glob(pattern: string): Promise<string[]> {
    const results: string[] = [];
    const seenPaths = new Set<string>();

    // Convert glob pattern to prefix for S3
    const prefix = pattern.split("*")[0].replace(/^\//, "");
    const s3Prefix =
      this.config.prefix === "/" ? prefix : this.config.prefix + prefix;

    let continuationToken: string | undefined;

    do {
      const response = await this.client.send(
        new ListObjectsV2Command({
          Bucket: this.config.bucket,
          Prefix: s3Prefix,
          ContinuationToken: continuationToken,
        }),
      );

      // Add files
      if (response.Contents) {
        for (const object of response.Contents) {
          if (object.Key) {
            const path = this.fromS3Key(object.Key);

            // Add the file itself if it matches (skip directory markers)
            if (!object.Key.endsWith("/") && this.matchGlob(path, pattern)) {
              seenPaths.add(path);
            }

            // Also check parent directories for patterns like "*"
            if (pattern === "*" || pattern === "**") {
              const parts = path.split("/").filter(Boolean);
              for (let i = 1; i <= parts.length - 1; i++) {
                const dirPath = "/" + parts.slice(0, i).join("/");
                if (this.matchGlob(dirPath, pattern)) {
                  seenPaths.add(dirPath);
                }
              }
            }
          }
        }
      }

      continuationToken = response.NextContinuationToken;
    } while (continuationToken);

    return Array.from(seenPaths).sort();
  }

  async clear(): Promise<void> {
    const startTime = Date.now();
    const operationId = `clear-${startTime}`;

    this.events.emit(FileSystemEvents.OPERATION_START, {
      operation: "clear",
      path: "/",
      id: operationId,
    });

    try {
      // List and delete all objects
      const objectsToDelete: { Key: string }[] = [];
      let continuationToken: string | undefined;

      do {
        const response = await this.client.send(
          new ListObjectsV2Command({
            Bucket: this.config.bucket,
            Prefix: this.config.prefix === "/" ? "" : this.config.prefix,
            ContinuationToken: continuationToken,
          }),
        );

        if (response.Contents) {
          for (const object of response.Contents) {
            if (object.Key) {
              objectsToDelete.push({ Key: object.Key });
            }
          }
        }

        continuationToken = response.NextContinuationToken;
      } while (continuationToken);

      // Delete in batches
      const batchSize = 1000;
      for (let i = 0; i < objectsToDelete.length; i += batchSize) {
        const batch = objectsToDelete.slice(i, i + batchSize);
        await this.client.send(
          new DeleteObjectsCommand({
            Bucket: this.config.bucket,
            Delete: { Objects: batch },
          }),
        );
      }

      this.events.emit(FileSystemEvents.OPERATION_END, {
        operation: "clear",
        path: "/",
        id: operationId,
        duration: Date.now() - startTime,
      });
    } catch (error) {
      this.events.emit(FileSystemEvents.OPERATION_ERROR, {
        operation: "clear",
        path: "/",
        error: error as Error,
      });
      throw error;
    }
  }

  async size(): Promise<number> {
    let totalSize = 0;
    let continuationToken: string | undefined;

    do {
      const response = await this.client.send(
        new ListObjectsV2Command({
          Bucket: this.config.bucket,
          Prefix: this.config.prefix === "/" ? "" : this.config.prefix,
          ContinuationToken: continuationToken,
        }),
      );

      if (response.Contents) {
        for (const object of response.Contents) {
          totalSize += object.Size || 0;
        }
      }

      continuationToken = response.NextContinuationToken;
    } while (continuationToken);

    return totalSize;
  }

  /**
   * Verifica se pode modificar um arquivo/objeto no S3.
   *
   * LIMITAÇÕES DO S3:
   * - S3 tem um modelo de permissões complexo (IAM, bucket policies, ACLs)
   * - Muitas vezes só sabemos se temos permissão quando tentamos a operação
   * - Este método faz uma estimativa baseada em:
   *   1. Se conseguimos acessar o objeto (HEAD request)
   *   2. Se recebemos erro 403 em qualquer operação
   *
   * @returns true se provavelmente pode modificar, false se definitivamente não pode
   */
  async canModify(path: string): Promise<boolean> {
    const normalized = normalizePath(path);

    try {
      const s3Key = this.toS3Key(normalized);

      // Tenta fazer um HEAD request no objeto
      try {
        await this.client.send(
          new HeadObjectCommand({
            Bucket: this.config.bucket,
            Key: s3Key,
          }),
        );
        // Se conseguimos ler metadata, provavelmente podemos modificar
        // MAS isso não é garantia - bucket policy pode permitir GET mas não PUT/DELETE
        return true;
      } catch (error: any) {
        if (error.name === "NoSuchKey") {
          // Arquivo não existe - verifica se podemos criar no diretório pai
          const parent = dirname(normalized);
          return this.canCreateIn(parent);
        }
        // Erro 403 = sem permissão
        if (
          error.name === "AccessDenied" ||
          error.$metadata?.httpStatusCode === 403
        ) {
          return false;
        }
        // Outros erros (rede, etc) - assumir que não pode
        return false;
      }
    } catch {
      return false;
    }
  }

  /**
   * Verifica se pode criar objetos em um "diretório" (prefixo) no S3.
   *
   * LIMITAÇÕES DO S3:
   * - S3 não tem diretórios reais, apenas prefixos
   * - Em modo "lenient", qualquer prefixo é válido
   * - Permissões são verificadas no nível do bucket, não do "diretório"
   *
   * @returns true se provavelmente pode criar, false se definitivamente não pode
   */
  async canCreateIn(parentPath: string): Promise<boolean> {
    const normalized = normalizePath(parentPath);

    try {
      // Para root, verifica se temos acesso de escrita ao bucket
      if (normalized === "/") {
        try {
          // Tenta listar o bucket - se conseguir, provavelmente pode escrever
          // MAS não é garantia - pode ter ListBucket mas não PutObject
          await this.client.send(
            new ListObjectsV2Command({
              Bucket: this.config.bucket,
              MaxKeys: 1,
            }),
          );
          return true;
        } catch (error: any) {
          if (
            error.name === "AccessDenied" ||
            error.$metadata?.httpStatusCode === 403
          ) {
            return false;
          }
          // Outros erros - assumir que não pode
          return false;
        }
      }

      // Em modo lenient, qualquer prefixo é válido para criar
      if (this.config.mode === "lenient") {
        // Ainda precisamos verificar acesso ao bucket
        return this.canCreateIn("/");
      }

      // Em modo strict, verifica se o "diretório" existe
      const exists = await this.exists(normalized);
      if (!exists) {
        return false;
      }

      // Verifica se é realmente um "diretório" (tem objetos com esse prefixo)
      const isDir = await this.isDirectory(normalized);
      if (!isDir) {
        return false; // É um arquivo, não pode criar "dentro" dele
      }

      // Se chegou aqui, provavelmente pode criar
      // (mas ainda depende das permissões reais do bucket)
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Tenta uma escrita "mais segura" no S3, mas NÃO é verdadeiramente atômica.
   *
   * LIMITAÇÕES DO S3:
   * - S3 não tem escrita atômica real
   * - Este método reduz a janela de inconsistência, mas não elimina
   * - Durante o processo, o arquivo pode existir em ambos os locais
   * - Se falhar no meio, pode deixar lixo (arquivo temporário)
   *
   * PROCESSO:
   * 1. Upload para arquivo temporário
   * 2. Copy para destino (substitui se existir)
   * 3. Delete arquivo temporário
   *
   * @returns FileEntry do arquivo final
   */
  async writeFileAtomic(
    path: string,
    content: any,
    metadata?: FileMetadata,
  ): Promise<FileEntry> {
    const normalized = normalizePath(path);
    const tempPath = `${normalized}.tmp${Date.now()}`;

    try {
      // 1. Upload para arquivo temporário
      await this.writeFile(tempPath, content, metadata);

      // 2. Copy para destino final
      // CopyObject substitui o destino se existir, mas NÃO é atômico no sentido POSIX
      // Durante a cópia, leitores podem ver estado parcial
      const s3TempKey = this.toS3Key(tempPath);
      const s3FinalKey = this.toS3Key(normalized);

      await this.client.send(
        new CopyObjectCommand({
          Bucket: this.config.bucket,
          CopySource: `${this.config.bucket}/${s3TempKey}`,
          Key: s3FinalKey,
          MetadataDirective: "COPY",
        }),
      );

      // 3. Remove o arquivo temporário
      await this.deleteFile(tempPath);

      // 4. Lê e retorna o arquivo final
      return await this.readFile(normalized);
    } catch (error) {
      // Tenta limpar arquivo temporário em caso de erro
      try {
        await this.deleteFile(tempPath);
      } catch {
        // Se falhar o cleanup, arquivo temporário ficará órfão
        // Considere implementar uma rotina de limpeza periódica
      }
      throw error;
    }
  }
}
