import {
  BaseService,
  type BaseServiceContext,
  throwAppError,
} from '@learnitive/base-utils';
import {
  FILE_STORAGE_MAX_FILE_SIZE_BYTES,
  SERVICE_NAME,
  WORKSPACE_STORAGE_TYPE,
} from '@learnitive/constants';
import { OpfsProjectStorage } from './project-storage-opfs';
import type {
  BaseFileStorageProvider,
  FileStat,
  FileStorageChangeEvent,
} from '@learnitive/types';
import {
  copyBytesToArrayBuffer,
  projectFilePath,
  supportsOpfsProjectStorage,
} from '@learnitive/project-storage';
import { WsPath } from '@learnitive/ws-path';
import { assertSameWorkspaceRename } from './file-storage-utils';

type Config = {
  onChange: (event: FileStorageChangeEvent) => void;
};

export class FileStorageOpfs
  extends BaseService
  implements BaseFileStorageProvider
{
  public readonly workspaceType = WORKSPACE_STORAGE_TYPE.BrowserOPFS;
  public readonly maxFileSizeBytes = FILE_STORAGE_MAX_FILE_SIZE_BYTES.browserOpfs;

  private readonly projectStorage = new Map<string, OpfsProjectStorage>();

  constructor(context: BaseServiceContext, dependencies: null, private config: Config) {
    super(SERVICE_NAME.fileStorageOpfsService, context, dependencies);
  }

  async hookMount(): Promise<void> {
    if (!supportsOpfsProjectStorage()) {
      this.logger.warn('OPFS project storage is not available in this browser');
    }
    this.addCleanup(() => this.projectStorage.clear());
  }

  async createFile(wsPath: string, file: File): Promise<void> {
    await this.mountPromise;
    const { storage, path } = this.resolve(wsPath);
    if ((await storage.readFile(path)) !== undefined) {
      throwAppError('error::file:already-existing', 'File already exists', {
        wsPath,
      });
    }
    await storage.writeFile(path, new Uint8Array(await file.arrayBuffer()));
    this.emitChange({ type: 'create', wsPath });
  }

  async deleteFile(wsPath: string): Promise<void> {
    await this.mountPromise;
    const { storage, path } = this.resolve(wsPath);
    await storage.deleteFile(path);
    this.emitChange({ type: 'delete', wsPath });
  }

  async fileExists(wsPath: string): Promise<boolean> {
    await this.mountPromise;
    const { storage, path } = this.resolve(wsPath);
    return (await storage.readFile(path)) !== undefined;
  }

  async fileStat(wsPath: string): Promise<FileStat> {
    await this.mountPromise;
    const { storage, path } = this.resolve(wsPath);
    const entry = (await storage.listEntries()).find(
      (candidate) => candidate.kind === 'file' && candidate.path === path,
    );
    if (!entry || entry.kind !== 'file') {
      throwAppError(
        'error::file-storage:file-does-not-exist',
        'Cannot stat file as it does not exist',
        { wsPath, storage: this.name },
      );
    }
    const modifiedAt = entry.modifiedAt ?? Date.now();
    return { ctime: modifiedAt, mtime: modifiedAt };
  }

  async readFile(wsPath: string): Promise<File | undefined> {
    await this.mountPromise;
    const { storage, path, fileName } = this.resolve(wsPath);
    const data = await storage.readFile(path);
    return data === undefined ? undefined : new File([copyBytesToArrayBuffer(data)], fileName);
  }

  async listAllFiles(
    wsName: string,
    abortSignal: AbortSignal,
  ): Promise<string[]> {
    await this.mountPromise;
    const storage = this.getProjectStorage(wsName);
    const entries = await storage.listEntries(abortSignal);
    const result: string[] = [];
    for (const entry of entries) {
      abortSignal.throwIfAborted();
      if (entry.kind !== 'file') {
        continue;
      }
      const wsPath = WsPath.safeFromParts(wsName, entry.path).data?.asFile();
      if (wsPath) {
        result.push(wsPath.wsPath);
      }
    }
    return result.sort((a, b) => a.localeCompare(b));
  }

  async renameFile(
    wsPath: string,
    { newWsPath }: { newWsPath: string },
  ): Promise<void> {
    await this.mountPromise;
    assertSameWorkspaceRename(wsPath, newWsPath);
    const source = this.resolve(wsPath);
    const destination = this.resolve(newWsPath);
    if ((await destination.storage.readFile(destination.path)) !== undefined) {
      throwAppError('error::file:already-existing', 'File already exists', {
        wsPath: newWsPath,
      });
    }
    await source.storage.moveFile(source.path, destination.path);
    this.emitChange({
      type: 'rename',
      oldWsPath: wsPath,
      newWsPath,
    });
  }

  async writeFile(wsPath: string, file: File): Promise<void> {
    await this.mountPromise;
    const { storage, path } = this.resolve(wsPath);
    if ((await storage.readFile(path)) === undefined) {
      throwAppError(
        'error::file-storage:file-does-not-exist',
        'Cannot write file as it does not exist',
        { wsPath, storage: this.name },
      );
    }
    await storage.writeFile(path, new Uint8Array(await file.arrayBuffer()));
    this.emitChange({ type: 'update', wsPath });
  }

  private getProjectStorage(wsName: string): OpfsProjectStorage {
    let storage = this.projectStorage.get(wsName);
    if (!storage) {
      storage = new OpfsProjectStorage(wsName);
      this.projectStorage.set(wsName, storage);
    }
    return storage;
  }

  private resolve(wsPath: string): {
    storage: OpfsProjectStorage;
    path: ReturnType<typeof projectFilePath>;
    fileName: string;
  } {
    const filePath = WsPath.assertFile(wsPath);
    return {
      storage: this.getProjectStorage(filePath.wsName),
      path: projectFilePath(filePath.path),
      fileName: filePath.fileName,
    };
  }

  private emitChange(event: FileStorageChangeEvent): void {
    this.config.onChange(event);
  }
}
