import {
  copyBytesToArrayBuffer,
  projectDirectoryPath,
  projectFilePath,
  projectParentDirectory,
  type ProjectDirectoryPath,
  type ProjectEntry,
  type ProjectFilePath,
  type ProjectStorageCapabilities,
  type ProjectStorageProvider,
} from '@learnitive/project-storage';

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

function isNotFoundError(error: unknown): boolean {
  return error instanceof DOMException && error.name === 'NotFoundError';
}

function splitDirectoryPath(path: ProjectDirectoryPath): string[] {
  return path.slice(0, -1).split('/').filter(Boolean);
}


function isDirectoryHandle(
  handle: FileSystemHandle,
): handle is FileSystemDirectoryHandle {
  return handle.kind === 'directory' && 'getDirectoryHandle' in handle;
}

function isFileHandle(handle: FileSystemHandle): handle is FileSystemFileHandle {
  return handle.kind === 'file' && 'getFile' in handle;
}

function directoryEntries(
  directory: FileSystemDirectoryHandle,
): AsyncIterable<readonly [string, FileSystemHandle]> {
  const entriesMethod: unknown = Reflect.get(directory, 'entries');
  if (typeof entriesMethod !== 'function') {
    throw new Error('Directory iteration is unavailable in this browser');
  }
  const iterable: unknown = Reflect.apply(entriesMethod, directory, []);
  if (
    !iterable ||
    typeof iterable !== 'object' ||
    !(Symbol.asyncIterator in iterable)
  ) {
    throw new Error('Directory iteration returned an invalid result');
  }
  return iterable as AsyncIterable<readonly [string, FileSystemHandle]>;
}

/**
 * Binary-safe project storage over a File System Access API directory handle.
 * Both OPFS and user-selected native folders use the same file semantics so
 * project import/export and future Git code cannot diverge by storage mode.
 */
export abstract class DirectoryHandleProjectStorage
  implements ProjectStorageProvider
{
  abstract readonly capabilities: ProjectStorageCapabilities;

  async listEntries(signal?: AbortSignal): Promise<ProjectEntry[]> {
    throwIfAborted(signal);
    const root = await this.getRootDirectory();
    const entries: ProjectEntry[] = [];
    await this.walkDirectory(root, '', entries, signal);
    throwIfAborted(signal);
    return entries.sort((a, b) => a.path.localeCompare(b.path));
  }

  async readFile(
    path: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<Uint8Array | undefined> {
    throwIfAborted(signal);
    const { directory, fileName } = await this.getParentDirectoryForFile(
      path,
      false,
    );
    if (!directory) {
      return undefined;
    }
    try {
      const handle = await directory.getFileHandle(fileName);
      const file = await handle.getFile();
      throwIfAborted(signal);
      return new Uint8Array(await file.arrayBuffer());
    } catch (error) {
      if (isNotFoundError(error)) {
        return undefined;
      }
      throw error;
    }
  }

  async writeFile(
    path: ProjectFilePath,
    data: Uint8Array,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    const { directory, fileName } = await this.getParentDirectoryForFile(
      path,
      true,
    );
    if (!directory) {
      throw new Error(`Unable to create parent directory for ${path}`);
    }
    const handle = await directory.getFileHandle(fileName, { create: true });
    const writable = await handle.createWritable();
    try {
      throwIfAborted(signal);
      await writable.write(copyBytesToArrayBuffer(data));
      throwIfAborted(signal);
      await writable.close();
    } catch (error) {
      await writable.abort().catch(() => undefined);
      throw error;
    }
  }

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

  async deleteFile(
    path: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    const { directory, fileName } = await this.getParentDirectoryForFile(
      path,
      false,
    );
    if (!directory) {
      return;
    }
    try {
      await directory.removeEntry(fileName);
    } catch (error) {
      if (!isNotFoundError(error)) {
        throw error;
      }
    }
  }

  async deleteDirectory(
    path: ProjectDirectoryPath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    const segments = splitDirectoryPath(path);
    const name = segments.pop();
    if (!name) {
      return;
    }
    const parent = await this.getDirectoryFromSegments(segments, false);
    if (!parent) {
      return;
    }
    try {
      await parent.removeEntry(name, { recursive: true });
    } catch (error) {
      if (!isNotFoundError(error)) {
        throw error;
      }
    }
  }

  async moveFile(
    from: ProjectFilePath,
    to: ProjectFilePath,
    signal?: AbortSignal,
  ): Promise<void> {
    throwIfAborted(signal);
    if ((await this.readFile(to, signal)) !== undefined) {
      throw new Error(`Project file already exists: ${to}`);
    }
    const data = await this.readFile(from, signal);
    if (!data) {
      throw new Error(`Project file does not exist: ${from}`);
    }

    await this.writeFile(to, data, signal);
    try {
      await this.deleteFile(from, signal);
    } catch (error) {
      await this.deleteFile(to).catch(() => undefined);
      throw error;
    }
  }

  protected abstract getRootDirectory(): Promise<FileSystemDirectoryHandle>;

  private async getDirectory(
    path: ProjectDirectoryPath,
    create: boolean,
  ): Promise<FileSystemDirectoryHandle | undefined> {
    return this.getDirectoryFromSegments(splitDirectoryPath(path), create);
  }

  private async getDirectoryFromSegments(
    segments: readonly string[],
    create: boolean,
  ): Promise<FileSystemDirectoryHandle | undefined> {
    let current = await this.getRootDirectory();
    for (const segment of segments) {
      try {
        current = await current.getDirectoryHandle(segment, { create });
      } catch (error) {
        if (!create && isNotFoundError(error)) {
          return undefined;
        }
        throw error;
      }
    }
    return current;
  }

  private async getParentDirectoryForFile(
    path: ProjectFilePath,
    create: boolean,
  ): Promise<{
    directory: FileSystemDirectoryHandle | undefined;
    fileName: string;
  }> {
    const slash = path.lastIndexOf('/');
    const fileName = slash < 0 ? path : path.slice(slash + 1);
    const parent = projectParentDirectory(path);
    const directory = parent
      ? await this.getDirectory(parent, create)
      : await this.getRootDirectory();
    return { directory, fileName };
  }

  private async walkDirectory(
    directory: FileSystemDirectoryHandle,
    prefix: string,
    output: ProjectEntry[],
    signal?: AbortSignal,
  ): Promise<void> {
    for await (const [name, handle] of directoryEntries(directory)) {
      throwIfAborted(signal);
      const relativePath = prefix ? `${prefix}/${name}` : name;
      if (isDirectoryHandle(handle)) {
        output.push({
          kind: 'directory',
          path: projectDirectoryPath(relativePath),
        });
        await this.walkDirectory(handle, relativePath, output, signal);
        continue;
      }
      if (!isFileHandle(handle)) {
        throw new Error(`Unsupported filesystem entry: ${relativePath}`);
      }
      const file = await handle.getFile();
      output.push({
        kind: 'file',
        path: projectFilePath(relativePath),
        size: file.size,
        modifiedAt: file.lastModified,
      });
    }
  }
}
