import {
  projectDirectoryPath,
  type ProjectDirectoryPath,
  type ProjectFilePath,
  projectParentDirectory,
} from './project-path';
import type { ProjectEntry, ProjectStorageProvider } from './types';

function throwIfAborted(signal?: AbortSignal): void {
  signal?.throwIfAborted();
}

function copyBytes(data: Uint8Array): Uint8Array {
  return data.slice();
}

export class MemoryProjectStorage implements ProjectStorageProvider {
  readonly capabilities = {
    directFilesystem: false,
    persistent: false,
    requiresExport: false,
  } as const;

  private files = new Map<ProjectFilePath, { data: Uint8Array; modifiedAt: number }>();
  private directories = new Set<ProjectDirectoryPath>();

  async listEntries(signal?: AbortSignal): Promise<ProjectEntry[]> {
    throwIfAborted(signal);
    const result: ProjectEntry[] = [];
    for (const path of this.directories) {
      result.push({ kind: 'directory', path });
    }
    for (const [path, value] of this.files) {
      result.push({
        kind: 'file',
        path,
        size: value.data.byteLength,
        modifiedAt: value.modifiedAt,
      });
    }
    throwIfAborted(signal);
    return result.sort((a, b) => a.path.localeCompare(b.path));
  }

  async readFile(
    path: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<Uint8Array | undefined> {
    throwIfAborted(signal);
    const value = this.files.get(path);
    return value ? copyBytes(value.data) : undefined;
  }

  async writeFile(
    path: ProjectFilePath,
    data: Uint8Array,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    const parent = projectParentDirectory(path);
    if (parent) {
      this.ensureDirectoryTree(parent);
    }
    this.files.set(path, { data: copyBytes(data), modifiedAt: Date.now() });
  }

  async createDirectory(
    path: ProjectDirectoryPath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    this.ensureDirectoryTree(path);
  }

  async deleteFile(
    path: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    this.files.delete(path);
  }

  async deleteDirectory(
    path: ProjectDirectoryPath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    for (const filePath of this.files.keys()) {
      if (filePath.startsWith(path)) {
        this.files.delete(filePath);
      }
    }
    for (const directoryPath of [...this.directories]) {
      if (directoryPath === path || directoryPath.startsWith(path)) {
        this.directories.delete(directoryPath);
      }
    }
  }

  async moveFile(
    from: ProjectFilePath,
    to: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    if (this.files.has(to)) {
      throw new Error(`Project file already exists: ${to}`);
    }
    const value = this.files.get(from);
    if (!value) {
      throw new Error(`Project file does not exist: ${from}`);
    }
    const parent = projectParentDirectory(to);
    if (parent) {
      this.ensureDirectoryTree(parent);
    }
    this.files.set(to, { data: copyBytes(value.data), modifiedAt: Date.now() });
    this.files.delete(from);
  }

  private ensureDirectoryTree(path: ProjectDirectoryPath): void {
    const segments = path.slice(0, -1).split('/');
    let current = '';
    for (const segment of segments) {
      current = current ? `${current}/${segment}` : segment;
      this.directories.add(projectDirectoryPath(current));
    }
  }
}
