/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable no-dupe-class-members */
import { 
  IBackendInfo, ProjectKind, FolderKind, DataFileKind, ContextListOptions, ContextListResult, IFile,
  IFileCreateOptions, AccessOperation, IUser, 
  Folder, HttpProject, IAccessPatchInfo, IPatchInfo, IPatchRevision, IApplication, 
  IHttpHistory, IHttpHistoryBulkAdd, HistoryListOptions, Project, ListFileKind, DataNamespace, DataFile, 
  IBulkOperationResult, ContextChangeRecord,
  ContextDeleteEvent,
  ContextDeleteRecord,
  IProjectExecution,
  IProjectExecutionListOptions,
  ISpace,
  SpaceKind,
  Space,
  PermissionRole,
  ContextSpaceListOptions,
  IRevision,
  TrashEntry,
  TrashEntryCreate,
  ContextDeleteBulkEvent,
  FileAddOptions,
  FileBreadcrumb,
  IBackendObserveCommand,
  BackendCommandType,
  BackendCommandKind,
  RouteBuilder,
  BroadcastEvent,
  BackendErrorEvent,
  BroadcastFileData,
} from '@api-client/core/build/browser.js';
import { Patch } from '@api-client/json';
import { PlatformBindings } from './PlatformBindings.js';
import { Events } from '../../events/Events.js';
import { EventTypes } from '../../events/EventTypes.js';
import { IConfigEnvironment, IConfigInit } from '../../lib/config/Config.js';
import { ISessionInitInfo, HttpStore } from '../../store/HttpStore.js';
import { StoreBroadcast } from '../../http-client/store/StoreBroadcast.js';

export interface EnvironmentSettings {
  /**
   * Initializes the store configuration from the init data.
   * 
   * @param init The init data for the store environment.
   */
  init(init: IConfigInit): Promise<void>;

  /**
   * Reads the backend information about the store.
   * 
   * @param baseUri Store's base URI.
   */
  readStoreInfo(baseUri: string): Promise<IBackendInfo>;
}

export interface StoreAuthenticationInit {
  /**
   * The environment to authenticate. Default to globally set environment.
   */
  env?: IConfigEnvironment; 
  /**
   * Whether to update environment in application configuration after authentication.
   */
  updateEnvironment?: boolean;
  /**
   * Whether to force authentication even when the user is already authenticated.
   */
  force?: boolean;
}

interface StoreAuthentication {
  /**
   * Checks whether the user is authenticated in the store for the environment
   * @param env Optional environment if different than the global environment
   * @returns True if the user is authenticated in the store.
   */
  isAuthenticated(env?: IConfigEnvironment): Promise<boolean>;

  /**
   * Authenticates the global environment with the store.
   * 
   * @param updateEnvironment When true it stores the environment data when the session was renewed.
   */
  authenticate(options?: StoreAuthenticationInit): Promise<ISessionInitInfo>;

  getToken(): Promise<string | undefined>;
}

/**
 * The base class for API store bindings.
 */
export abstract class StoreBindings extends PlatformBindings {
  /**
   * This is used by the client side (main thread or the renderer process)
   * to define the current space the app is using. 
   * This space is used in some APIs instead of adding the space to the events system.
  */
  space = '';

  /**
   * The currently used environment for all store calls.
   * This can be changed via the `Events.Global.setEnv()`
   */
  globalEnvironment?: IConfigEnvironment;

  /**
   * Initialized with the `globalEnvironment`.
   * The store with initialized SDK for the global environment
   */
  store?: HttpStore;

  filesChannel = new BroadcastChannel(StoreBroadcast.files);

  mediaChannel = new BroadcastChannel(StoreBroadcast.media);

  spacesChannel = new BroadcastChannel(StoreBroadcast.spaces);

  trashChannel = new BroadcastChannel(StoreBroadcast.trash);

  historyChannel = new BroadcastChannel(StoreBroadcast.history);

  projectExeChannel = new BroadcastChannel(StoreBroadcast.projectRuns);

  initialized = false;

  async initializeStore(): Promise<void> {
    // ...
  }

  async initialize(): Promise<void> {
    if (this.initialized) {
      return;
    }
    this.initialized = true;
    const target = document.body;

    window.addEventListener(EventTypes.Store.initEnvironment, this.handleInitializeEnvironment.bind(this));
    window.addEventListener(EventTypes.Store.info, this.handleStoreInfo.bind(this));
    window.addEventListener(EventTypes.Store.Global.setEnv, this.setGlobalEnvHandler.bind(this));
    window.addEventListener(EventTypes.Store.Global.getEnv, this.getGlobalEnvHandler.bind(this));
    
    // Auth
    window.addEventListener(EventTypes.Store.Auth.authenticate, this.storeAuthHandler.bind(this));
    window.addEventListener(EventTypes.Store.Auth.isAuthenticated, this.storeIsAuthenticatedHandler.bind(this));
    window.addEventListener(EventTypes.Store.Auth.getToken, this.getTokenHandler.bind(this));

    // Files
    window.addEventListener(EventTypes.Store.File.list, this.handleFileList.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.listShared, this.handleSharedFileList.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.create, this.handleFileCreate.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.createDefault, this.fileCreateDefaultHandler.bind(this));
    window.addEventListener(EventTypes.Store.File.read, this.handleFileRead.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.patch, this.handleFilePatch.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.delete, this.handleFileDelete.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.patchUsers, this.handleFilePatchUser.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.listUsers, this.handleListFileUsers.bind(this) as EventListener);
    window.addEventListener(EventTypes.Store.File.breadcrumbs, this.handleFileBreadcrumbs.bind(this) as EventListener);

    // User
    window.addEventListener(EventTypes.Store.User.me, this.handlerMe.bind(this));
    window.addEventListener(EventTypes.Store.User.list, this.handleUsersList.bind(this));
    window.addEventListener(EventTypes.Store.User.read, this.handleUserRead.bind(this));

    // history
    target.addEventListener(EventTypes.Store.History.create, this.handleHistoryCreate.bind(this));
    target.addEventListener(EventTypes.Store.History.createBulk, this.handleHistoryCreateBulk.bind(this));
    target.addEventListener(EventTypes.Store.History.delete, this.handleHistoryDelete.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.History.deleteBulk, this.handleHistoryDeleteBulk.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.History.list, this.handleHistoryList.bind(this));
    target.addEventListener(EventTypes.Store.History.read, this.handleHistoryRead.bind(this));
    target.addEventListener(EventTypes.Store.History.clearSpace, this.handleHistoryClearSpace.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.History.clearProject, this.handleHistoryClearProject.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.History.clearRequest, this.handleHistoryClearRequest.bind(this) as EventListener);

    // project runs
    target.addEventListener(EventTypes.Store.ProjectRuns.add, this.handleAddProjectExecution.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.list, this.handleProjectExecutionList.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.read, this.handleProjectExecutionRead.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.delete, this.handleProjectExecutionDelete.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.deleteBulk, this.handleProjectExecutionDeleteBulk.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.clearSpace, this.handleProjectExecutionClearSpace.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.clearProject, this.handleProjectExecutionClearProject.bind(this) as EventListener);
    target.addEventListener(EventTypes.Store.ProjectRuns.clearParent, this.handleProjectExecutionClearFolder.bind(this) as EventListener);
  }

  abstract environment: EnvironmentSettings;

  auth: StoreAuthentication = {
    isAuthenticated: async (env?: IConfigEnvironment): Promise<boolean> => {
      let { store } = this;
      if (env) {
        store = new HttpStore(env);
      }
      if (!store) {
        throw new Error(`Environment is not set.`);
      }
      return store.isAuthenticated(env);
    },

    authenticate: async (options: StoreAuthenticationInit = {}): Promise<ISessionInitInfo> => {
      const { env, force, updateEnvironment } = options;
      let { store } = this;
      if (env) {
        store = new HttpStore(env);
      }
      if (!store) {
        throw new Error(`Environment is not set.`);
      }
      const result = await store.getStoreSessionToken(undefined, force);
      if (updateEnvironment && result.new) {
        await Events.Config.Environment.update(env || this.globalEnvironment as IConfigEnvironment);
      }
      return result;
    },

    /**
     * @returns The current store token or undefined if no store is set.
     */
    getToken: async (): Promise<string | undefined> => {
      const { store } = this;
      return store && store.sdk && store.sdk.token || undefined;
    },
  }

  global = {
    /**
     * Sets the store environment to be used globally by the application.
     * This initializes the store with the configuration.
     * 
     * Note, this won't initialize the session with the store.
     * 
     * @param env The environment to set. Note, it must have the `location` set.
     */
    set: async (env: IConfigEnvironment): Promise<void> => {
      if (!env.location) {
        throw new Error(`The location is required for store environment configuration.`);
      }
      this.globalEnvironment = env;
      const store = new HttpStore(env);
      this.store = store;
    },

    get: async (): Promise<IConfigEnvironment> => {
      const { globalEnvironment } = this;
      if (!globalEnvironment) {
        throw new Error(`The current environment is not set.`);
      }
      return globalEnvironment;
    },
  }

  protected handleInitializeEnvironment(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    const copy = { ...e.detail };
    e.detail.result = this.environment.init(copy);
  }

  protected handleStoreInfo(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.environment.readStoreInfo(e.detail.baseUri);
  }

  protected storeAuthHandler(input: Event): void {
    const e = input as CustomEvent<{ options?: StoreAuthenticationInit, result: Promise<ISessionInitInfo> }>;
    e.preventDefault();
    e.detail.result = this.auth.authenticate(e.detail.options);
  }

  protected storeIsAuthenticatedHandler(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.auth.isAuthenticated(e.detail.env);
  }

  protected setGlobalEnvHandler(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.global.set(e.detail.env);
  }

  protected getGlobalEnvHandler(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.global.get();
  }

  protected getTokenHandler(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.auth.getToken();
  }

  protected handleFileList(event: CustomEvent<{ kinds?: ListFileKind[], options?: ContextListOptions, result: Promise<ContextListResult<IFile>> }>): void {
    event.preventDefault();
    const { kinds, options } = event.detail;
    const { space } = this;
    event.detail.result = this.getStore().sdk.file.list(space, kinds as ListFileKind[], options);
  }

  protected handleSharedFileList(event: CustomEvent<{ kinds?: ListFileKind[], options?: ContextListOptions, result: Promise<ContextListResult<IFile>> }>): void {
    event.preventDefault();
    const { kinds, options } = event.detail;
    const { space } = this;
    event.detail.result = this.getStore().sdk.shared.list(space, kinds, options);
  }

  protected handleFileCreate(event: CustomEvent<{ meta: IFile, media?: unknown, opts?: IFileCreateOptions, result: Promise<ContextChangeRecord<IFile>> }>): void {
    event.preventDefault();
    const { meta, media, opts } = event.detail;
    if (media) {
      event.detail.result = this.getStore().sdk.file.create(meta, media, this.space, opts);
    } else {
      event.detail.result = this.getStore().sdk.file.createMeta(meta, this.space, opts);
    }
  }

  protected fileCreateDefaultHandler(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.createDefaultFile(e.detail.name, e.detail.kind, e.detail.opts);
  }

  protected handleFileRead(event: CustomEvent<{ key: string, media: boolean, result: Promise<IFile | unknown> }>): void {
    event.preventDefault();
    const { media, key } = event.detail;
    if (media) {
      event.detail.result = this.getStore().sdk.file.read(this.space, key, media as true);
    } else {
      event.detail.result = this.getStore().sdk.file.read(this.space, key, media as false);
    }
  }

  protected handleFilePatch(event: CustomEvent<{ key: string, id: string, value: Patch.JsonPatch, app: IApplication, media?: boolean, result: Promise<IPatchRevision> }>): void {
    event.preventDefault();
    const { key, id, value, app, media } = event.detail;
    const info: IPatchInfo = {
      app: app.code,
      appVersion: app.version,
      patch: value,
      id,
    };

    event.detail.result = this.getStore().sdk.file.patch(this.space, key, info, media as false);
  }

  protected handleFileDelete(event: CustomEvent<{ key: string, result: Promise<string> }>): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.file.delete(this.space, event.detail.key);
  }

  protected handleFilePatchUser(event: CustomEvent<{ key: string, id: string, value: AccessOperation[], app: IApplication, result: Promise<void> }>): void {
    event.preventDefault();
    const { key, id, value } = event.detail;
    const info: IAccessPatchInfo = {
      patch: value,
      id,
    };
    event.detail.result = this.getStore().sdk.file.patchUsers(this.space, key, info);
  }

  protected handleListFileUsers(event: CustomEvent<{ key: string, result: Promise<ContextListResult<IUser>> }>): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.file.listUsers(this.space, event.detail.key);
  }

  protected handleFileBreadcrumbs(event: CustomEvent<{ key: string, result: Promise<ContextListResult<FileBreadcrumb>> }>): void {
    event.preventDefault();
    event.detail.result = this.files.breadcrumbs(event.detail.key)
    // this.getStore().sdk.file.breadcrumbs(this.space, event.detail.key);
  }

  protected handlerMe(input: Event): void {
    const e = input as CustomEvent<{ result: Promise<IUser> }>;
    e.preventDefault();
    e.detail.result = this.users.me();
  }

  protected handleUsersList(input: Event): void {
    const e = input as CustomEvent<{ options?: ContextListOptions, result: Promise<ContextListResult<IUser>> }>;
    e.preventDefault();
    e.detail.result = this.users.list(e.detail.options);
  }


  protected handleUserRead(input: Event): void {
    const e = input as CustomEvent<{ key: string, result: Promise<IUser> }>;
    e.preventDefault();
    e.detail.result = this.users.read(e.detail.key);
  }

  protected handleHistoryCreate(input: Event): void {
    const e = input as CustomEvent<{ history: IHttpHistory, result: Promise<ContextChangeRecord<IHttpHistory>> }>;
    e.preventDefault();
    const cp = { ...e.detail.history };
    if (!cp.space) {
      cp.space = this.space;
    }
    e.detail.result = this.getStore().sdk.history.add(cp);
  }

  protected handleHistoryCreateBulk(input: Event): void {
    const e = input as CustomEvent<{ info: IHttpHistoryBulkAdd, result: Promise<IBulkOperationResult<ContextChangeRecord<IHttpHistory>>> }>;
    e.preventDefault();
    const cp = { ...e.detail.info };
    if (!cp.space) {
      cp.space = this.space;
    }
    e.detail.result = this.getStore().sdk.history.createBulk(cp);
  }

  protected handleHistoryDelete(input: Event): void {
    const e = input as ContextDeleteEvent;
    e.preventDefault();
    e.detail.result = this.deleteHistory(e.detail.key);
  }

  protected handleHistoryList(input: Event): void {
    const e = input as CustomEvent<{ init: HistoryListOptions, result: Promise<ContextListResult<IHttpHistory>> }>;
    e.preventDefault();
    const cp = { ...e.detail.init } as HistoryListOptions;
    if (!cp.space) {
      cp.space = this.space;
    }
    e.detail.result = this.getStore().sdk.history.list(cp);
  }

  protected handleHistoryRead(input: Event): void {
    const e = input as CustomEvent;
    e.preventDefault();
    e.detail.result = this.getStore().sdk.history.read(e.detail.key);
  }

  protected handleHistoryDeleteBulk(event: ContextDeleteBulkEvent): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.history.deleteBulk(event.detail.keys);
  }

  protected handleHistoryClearSpace(event: CustomEvent<{ result: Promise<void>, space: string }>): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.history.clearSpace(event.detail.space);
  }

  protected handleHistoryClearProject(event: CustomEvent<{ result: Promise<void>, space: string, project: string }>): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.history.clearProject(event.detail.space, event.detail.project);
  }

  protected handleHistoryClearRequest(event: CustomEvent<{ result: Promise<void>, space: string, project: string, request: string }>): void {
    event.preventDefault();
    event.detail.result = this.getStore().sdk.history.clearRequest(event.detail.space, event.detail.project, event.detail.request);
  }

  async deleteHistory(key: string): Promise<ContextDeleteRecord> {
    await this.getStore().sdk.history.delete(key);
    return {
      key,
    };
  }

  protected handleAddProjectExecution(event: CustomEvent<{ item: IProjectExecution, result: Promise<ContextChangeRecord<IProjectExecution>> }>): void {
    event.preventDefault();
    const { item } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.add(this.space, item);
  }

  protected handleProjectExecutionRead(event: CustomEvent<{ key: string, result: Promise<IProjectExecution> }>): void {
    event.preventDefault();
    const { key } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.read(key);
  }

  protected handleProjectExecutionList(event: CustomEvent<{ options: IProjectExecutionListOptions, result: Promise<ContextListResult<IProjectExecution>> }>): void {
    event.preventDefault();
    const { options } = event.detail;
    const cp = { ...options };
    if (!cp.space) {
      cp.space = this.space;
    }
    event.detail.result = this.getStore().sdk.projectExecution.list(cp);
  }

  protected handleProjectExecutionDelete(event: CustomEvent<{ key: string, result: Promise<void> }>): void {
    event.preventDefault();
    const { key } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.delete(key);
  }

  protected handleProjectExecutionDeleteBulk(event: ContextDeleteBulkEvent): void {
    event.preventDefault();
    const { keys } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.deleteBulk(keys);
  }

  protected handleProjectExecutionClearSpace(event: CustomEvent<{ space: string, result: Promise<void> }>): void {
    event.preventDefault();
    const { space } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.clearSpace(space);
  }

  protected handleProjectExecutionClearProject(event: CustomEvent<{ space: string, project: string, result: Promise<void> }>): void {
    event.preventDefault();
    const { space, project } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.clearProject(space, project);
  }

  protected handleProjectExecutionClearFolder(event: CustomEvent<{ space: string, project: string, folder: string, result: Promise<void> }>): void {
    event.preventDefault();
    const { space, project, folder } = event.detail;
    event.detail.result = this.getStore().sdk.projectExecution.clearParent(space, project, folder);
  }

  /**
   * A logic that creates an empty (default) file from its name and the file kind.
   * 
   * @param name The name of the file.
   * @param kind The kind of the file.
   * @param opts Optional create options
   * @returns The key of the created file.
   */
  async createDefaultFile(space: string, name: string, kind: string, opts?: IFileCreateOptions): Promise<ContextChangeRecord<IFile>> {
    switch (kind) {
      case ProjectKind: return this.createDefaultProject(space, name, opts);
      case FolderKind: return this.createDefaultFolder(space, name, opts);
      case DataFileKind: return this.createDefaultDataNamespace(space, name, opts);
      default: 
        throw new Error(`Unrecognized file kind: ${kind}`);
    }
  }

  /**
   * Creates a default HttpProject from a name
   * 
   * @param name The name of the project
   * @param opts Create options.
   * @returns The key of the created file.
   */
  async createDefaultProject(space: string, name: string, opts?: IFileCreateOptions): Promise<ContextChangeRecord<IFile>> {
    const { store } = this;
    if (!store) {
      throw new Error(`Environment is not set.`);
    }
    const media = HttpProject.fromName(name);
    const file = Project.fromProject(media, space);
    return store.sdk.file.create(file.toJSON(), media.toJSON(), space, opts);
  }

  /**
   * Creates a default DataNamespace from a name
   * 
   * @param name The name of the DataNamespace
   * @param opts Create options.
   * @returns The key of the created file.
   */
  async createDefaultDataNamespace(space: string, name: string, opts?: IFileCreateOptions): Promise<ContextChangeRecord<IFile>> {
    const { store } = this;
    if (!store) {
      throw new Error(`Environment is not set.`);
    }
    const media = DataNamespace.fromName(name);
    const file = DataFile.fromDataNamespace(media, space);
    return store.sdk.file.create(file.toJSON(), media.toJSON(), space, opts);
  }

  /**
   * Creates a default Folder from a name
   * 
   * @param name The name of the space
   * @param opts Create options.
   * @returns The key of the created file.
   */
  async createDefaultFolder(space: string, name: string, opts?: IFileCreateOptions): Promise<ContextChangeRecord<IFile>> {
    const { store } = this;
    if (!store) {
      throw new Error(`Environment is not set.`);
    }
    const folder = Folder.fromName(name, space);
    return store.sdk.file.createMeta(folder.toJSON(), space, opts);
  }

  getStore(): HttpStore {
    const { store } = this;
    if (!store) {
      throw new Error(`Environment is not set.`);
    }
    return store;
  }

  spaces = {
    list: (options?: ContextListOptions): Promise<ContextListResult<ISpace>> => this.getStore().sdk.spaces.list(options),
    add: (nameOrObject: string | ISpace): Promise<ContextChangeRecord<ISpace>> => {
      let space: ISpace;
      if (typeof nameOrObject === 'string') {
        space = Space.fromName(nameOrObject).toJSON();
      } else {
        space = nameOrObject as ISpace;
      }
      return this.getStore().sdk.spaces.add(space);
    },
    read: (key: string): Promise<ISpace> => this.getStore().sdk.spaces.read(key),
    delete: async (key: string): Promise<ContextDeleteRecord> => {
      await this.getStore().sdk.spaces.delete(key);
      return {
        key,
        kind: SpaceKind,
      };
    },
    addUser: (space: string, otherUserKey: string, role: PermissionRole, expires?: number): Promise<void> => this.getStore().sdk.spaces.addUser(this.space || space, otherUserKey, role, expires),
    removeUser: (space: string, otherUserKey: string): Promise<void> => this.getStore().sdk.spaces.removeUser(this.space || space, otherUserKey),
    patchUsers: (space: string, value: IAccessPatchInfo): Promise<void> => this.getStore().sdk.spaces.patchUsers(this.space || space, value),
  }

  trash = {
    add: (entry: TrashEntryCreate, space: string): Promise<ContextChangeRecord<TrashEntry>> => this.getStore().sdk.trash.add(entry, this.space || space),
    list: (options: ContextSpaceListOptions): Promise<ContextListResult<TrashEntry>> => {
      const cp = { ...options } as ContextSpaceListOptions;
      if (!cp.space) {
        cp.space = this.space;
      }
      return this.getStore().sdk.trash.list(cp);
    },
    delete: async (key: string): Promise<ContextDeleteRecord> => {
      await this.getStore().sdk.trash.delete(key);
      return { key };
    },
    batchDelete: (keys: string[]): Promise<IBulkOperationResult<ContextDeleteRecord>> => this.getStore().sdk.trash.batchDelete(keys),
    restore: (key: string): Promise<ContextChangeRecord<IFile>> => this.getStore().sdk.trash.restore(key),
    batchRestore: (keys: string[]): Promise<IBulkOperationResult<ContextChangeRecord<IFile>>> => this.getStore().sdk.trash.batchDelete(keys),
    empty: (space: string): Promise<void> => this.getStore().sdk.trash.empty(this.space || space),
  }

  revisions = {
    list: (key: string, options: ContextSpaceListOptions): Promise<ContextListResult<IRevision>> => this.getStore().sdk.revisions.list(key, options),
  }

  media = {
    add: (key: string, contents: unknown, mime: string): Promise<void> => this.getStore().sdk.file.createMedia(contents, this.space, key, { mime }),
    read: (key: string): Promise<unknown> => this.getStore().sdk.file.read(this.space, key, true),
    patch: (key: string, info: IPatchInfo): Promise<IPatchRevision> => this.getStore().sdk.file.patch(this.space, key, info, true),
  }

  files = {
    list: (options: ContextListOptions, kinds?: ListFileKind[]): Promise<ContextListResult<IFile>> => this.getStore().sdk.file.list(this.space, kinds, options),
    add: (input: IFile, opts?: FileAddOptions): Promise<ContextChangeRecord<IFile>> => this.getStore().sdk.file.createMeta(input, this.space, opts),
    read: (key: string): Promise<IFile> => this.getStore().sdk.file.read(this.space, key, false),
    readBulk: (keys: string[]): Promise<IBulkOperationResult<IFile>> => this.getStore().sdk.file.readBulk(this.space, keys),
    delete: (key: string): Promise<string> => this.getStore().sdk.file.delete(this.space, key),
    patch: (key: string, info: IPatchInfo): Promise<IPatchRevision> => this.getStore().sdk.file.patch(this.space, key, info, false),
    breadcrumbs: (key: string): Promise<ContextListResult<FileBreadcrumb>> => this.getStore().sdk.file.breadcrumbs(this.space, key),
  }

  shared = {
    list: (options: ContextListOptions, kinds?: ListFileKind[]): Promise<ContextListResult<IFile>> => this.getStore().sdk.shared.list(this.space, kinds, options),
  }

  users = {
    list: (options?: ContextListOptions): Promise<ContextListResult<IUser>> => this.getStore().sdk.user.list(options),
    me: (): Promise<IUser> => this.getStore().sdk.user.me(),
    read: (key: string): Promise<IUser> => this.getStore().sdk.user.read(key),
  }
  
  async observeSpaces(): Promise<void> {
    await this.observePath(RouteBuilder.spaces());
  }

  async unobserveSpaces(): Promise<void> {
    await this.unobservePath(RouteBuilder.spaces());
  }

  async observeFiles(): Promise<void> {
    await this.observePath(RouteBuilder.files());
  }

  async unobserveFiles(): Promise<void> {
    await this.unobservePath(RouteBuilder.files());
  }

  async observeFile(key: string, alt: 'media' | 'meta' = 'meta'): Promise<void> {
    const path = RouteBuilder.file(key)
    await this.observePath(`${path}?alt=${alt}`);
  }

  async unobserveFile(key: string, alt: 'media' | 'meta' = 'meta'): Promise<void> {
    const path = RouteBuilder.file(key)
    await this.unobservePath(`${path}?alt=${alt}`);
  }

  async observeHistory(): Promise<void> {
    await this.observePath(RouteBuilder.history());
  }

  async unobserveHistory(): Promise<void> {
    await this.unobservePath(RouteBuilder.history());
  }

  async observeProjectExecutions(): Promise<void> {
    await this.observePath(RouteBuilder.projectExecutions());
  }

  async unobserveProjectExecutions(): Promise<void> {
    await this.unobservePath(RouteBuilder.projectExecutions());
  }

  async observeTrash(): Promise<void> {
    await this.observePath(RouteBuilder.trash());
  }

  async unobserveTrash(): Promise<void> {
    await this.unobservePath(RouteBuilder.trash());
  }

  async sendSocket(ws: WebSocket, command: unknown): Promise<void> {
    ws.send(JSON.stringify(command));
  }

  protected async observePath(path: string): Promise<void> {
    const { space } = this;
    const url = new URL(path, 'http://localhost');
    url.searchParams.set('space', space);
    const fullPath = `${url.pathname}${url.search}`
    const socket = await this.getEventsSocket();
    const cmd: IBackendObserveCommand = {
      cmd: BackendCommandType.observe,
      kind: BackendCommandKind,
      path: fullPath,
    };
    await this.sendSocket(socket, cmd);
  }

  protected async unobservePath(path: string): Promise<void> {
    const socket = await this.getEventsSocket();
    const cmd: IBackendObserveCommand = {
      cmd: BackendCommandType.unobserve,
      kind: BackendCommandKind,
      path,
    };
    await this.sendSocket(socket, cmd);
  }

  eventsSocket?: WebSocket;

  async getEventsSocket(): Promise<WebSocket> {
    if (!this.eventsSocket) {
      const store = this.getStore();
      const { token } = store.sdk;
      const url = store.sdk.getUrl('/events');
      this.eventsSocket = await store.sdk.ws.createAndConnect(url.toString(), token) as WebSocket;
      this.eventsSocket.addEventListener('message', this.storeEventHandler.bind(this));
    }
    return this.eventsSocket;
  }

  async closeEventsSocket(): Promise<void> {
    const { eventsSocket } = this;
    if (!eventsSocket) {
      return;
    }
    this.eventsSocket = undefined;
    eventsSocket.close();
  }

  protected storeEventHandler(e: MessageEvent): void {
    const event = JSON.parse(e.data) as BroadcastEvent | BackendErrorEvent;
    if (event.type === 'error') {
      throw new Error(event.cause);
    }
    if (event.space && event.space !== this.space) {
      // we allow files here to support shared files.
      if (event.path === RouteBuilder.files()) {
        this.filesChannel.postMessage(event);
      }
      return;
    }
    if (event.path === RouteBuilder.spaces()) {
      this.spacesChannel.postMessage(event);
    } else if (event.path === RouteBuilder.files()) {
      this.filesChannel.postMessage(event);
    } else if (event.path.startsWith(RouteBuilder.files())) {
      const typed = event as BroadcastFileData & BroadcastEvent;
      if (typed.alt === 'media') {
        this.mediaChannel.postMessage(event);
      } else {
        this.filesChannel.postMessage(event);
      }
    } else if (event.path === RouteBuilder.trash()) {
      this.trashChannel.postMessage(event);
    } else if (event.path === RouteBuilder.history()) {
      this.historyChannel.postMessage(event);
    } else if (event.path === RouteBuilder.projectExecutions()) {
      this.projectExeChannel.postMessage(event);
    } else {
      // eslint-disable-next-line no-console
      console.warn(`Broadcast path not handled`, event.path, event)
    }
  }
}
