/* eslint-disable @typescript-eslint/ban-ts-comment */
import { get, set, del } from 'idb-keyval';
import { 
  AddConfigEnvironmentInit, EnvironmentConfiguration, EnvironmentsKey, 
  LocalConfiguration, SessionConfiguration, TelemetryConfiguration, 
  TelemetryKey 
} from '../../../src/bindings/base/ConfigurationBindings.js';
import { IoEvent, IoNotification } from '../../../src/bindings/base/PlatformBindings.js';
import { SharedWorkerThread } from '../../../src/bindings/base/SharedWorkerThread.js';
import { IConfigEnvironment, ITelemetryConfig, IEnvConfig, ConfigBroadcastChannel, ConfigBroadcast } from '../../../src/lib/config/Config.js';

/**
 * With the `environment-*` events we mimic the application's
 * communication system to all opened windows.
 * 
 * The environments configuration applies to all apps and all windows
 * so we distribute this to all.
 */
class AppConfig extends SharedWorkerThread {
  ports: MessagePort[] = [];

  postMessage(message: IoNotification | IoEvent): void {
    this.ports.forEach(p => p.postMessage(message));
  }

  broadcast = new BroadcastChannel(ConfigBroadcastChannel);

  local: LocalConfiguration = {
    /**
     * Reads a previously stored local value.
     * 
     * @param key The key under which the property was stored.
     * @returns The configuration value or undefined when not found.
     */
    get: async (key: string): Promise<unknown | undefined> => {
      const value = await get(key);
      if (value) {
        return value.value;
      }
      return undefined;
    },

    /**
     * Sets a config property that is permanently stored.
     * 
     * @param key The key under to store the value.
     * @param value The value to store. If this is not a primitive it will be serialized with `JSON.stringify()`.
     */
    set: async (key: string, value: unknown): Promise<void> => {
      const storedValue = {
        type: typeof value,
        value,
      };
      await set(key, storedValue);
      this.broadcast.postMessage({
        path: 'local.set',
        key,
        value,
      } as ConfigBroadcast);
    },

    /**
     * Deletes previously stored local value.
     * 
     * @param key The key under which the property was stored.
     */
    delete: async (key: string): Promise<void> => {
      await del(key);
      this.broadcast.postMessage({
        path: 'local.delete',
        key,
      } as ConfigBroadcast);
    }
  }

  session: SessionConfiguration = {
    get: async (key: string): Promise<unknown> => {
      const data = sessionStorage.getItem(key);
      if (data === null) {
        return undefined;
      }
      let restored: unknown | undefined;
      try {
        const parsed = JSON.parse(data);
        restored = parsed.value;
      } catch (e) {
        // ...
      }
      return restored;
    },

    set: async (key: string, value: unknown): Promise<void> => {
      const storedValue = {
        type: typeof value,
        value,
      };
      sessionStorage.setItem(key, JSON.stringify(storedValue));
      this.broadcast.postMessage({
        path: 'session.set',
        key,
        value,
      } as ConfigBroadcast);
    },

    delete: async (key: string): Promise<void> => {
      sessionStorage.removeItem(key);
      this.broadcast.postMessage({
        path: 'session.delete',
        key,
      } as ConfigBroadcast);
    }
  }

  telemetry: TelemetryConfiguration = {
    read: async (): Promise<ITelemetryConfig> => {
      let data = await get(TelemetryKey) as ITelemetryConfig;
      if (!data) {
        data = {
          level: 'noting',
        };
      }
      return data;
    },

    set: async (config: ITelemetryConfig): Promise<void> => {
      await set(TelemetryKey, config);
      this.broadcast.postMessage({
        path: 'telemetry.set',
        value: config,
      } as ConfigBroadcast);
    },
  }

  environment: EnvironmentConfiguration = {
    add: async (env: IConfigEnvironment, init: AddConfigEnvironmentInit = {}): Promise<void> => {
      const data = await this.environment.list();
      data.environments.push(env);
      if (init.asDefault) {
        data.current = env.key;
      }
      await set(EnvironmentsKey, data);
      this.broadcast.postMessage({
        path: 'environment.add',
        env, 
        init,
      } as ConfigBroadcast);
    },

    update: async (env: IConfigEnvironment): Promise<void> => {
      const data = await this.environment.list();
      const index = data.environments.findIndex(i => i.key === env.key);
      if (index < 0) {
        throw new Error(`The environment does not exist. Maybe use "add" instead?`);
      }
      data.environments[index] = env;
      await set(EnvironmentsKey, data);
      this.broadcast.postMessage({
        path: 'environment.update',
        env,
      } as ConfigBroadcast);
    },

    read: async (id?: string): Promise<IConfigEnvironment> => {
      const data = await this.environment.list();
      const key = id || data.current;
      if (!key) {
        throw new Error(`No default environment.`);
      }
      const env = data.environments.find(i => i.key === key);
      if (!env) {
        throw new Error(`The environment is not found. Reinitialize application configuration.`);
      }
      return env;
    },

    remove: async (id: string): Promise<void> => {
      const data = await this.environment.list();
      const index = data.environments.findIndex(i => i.key === id);
      if (index >= 0) {
        data.environments.splice(index, 1);
      }
      if (data.current === id) {
        delete data.current;
      }
      await set(EnvironmentsKey, data);
      this.broadcast.postMessage({
        path: 'environment.remove',
        id,
      } as ConfigBroadcast);
    },

    setDefault: async (id: string): Promise<void> => {
      const data = await this.environment.list();
      const env = data.environments.find(i => i.key === id);
      if (!env) {
        throw new Error(`The environment is not defined: ${id}`);
      }
      data.current = id;
      await set(EnvironmentsKey, data);
      this.broadcast.postMessage({
        path: 'environment.setDefault',
        id,
        env,
      } as ConfigBroadcast);
    },

    list: async (): Promise<IEnvConfig> => {
      let data = await get(EnvironmentsKey) as IEnvConfig;
      if (!data) {
        data = {
          environments: [],
        };
      }
      return data;
    }
  }
}

const instance = new AppConfig();
instance.initialize();
