import {
  type Observable,
  ReplaySubject,
  Subject,
  distinctUntilChanged,
  firstValueFrom,
  map,
  scan,
  startWith,
} from 'rxjs';

import { type AppPluginConfig } from '@grafana/data';

import { type ExtensionsLog, log } from '../logs/log';
import { deepFreeze } from '../utils';

export const MSG_CANNOT_REGISTER_READ_ONLY = 'Cannot register to a read-only registry';

export type PluginExtensionConfigs<T> = {
  pluginId: string;
  configs: T[];
};

export type RegistryType<T> = Record<string | symbol, T>;

// This is the base-class used by the separate specific registries.
export abstract class Registry<TRegistryValue extends object | unknown[] | Record<string | symbol, unknown>, TMapType> {
  // Used in cases when we would like to pass a read-only registry to plugin.
  // In these cases we are passing in the `registrySubject` to the constructor.
  // (If TRUE `initialState` is ignored.)
  private isReadOnly: boolean;
  // This is the subject that receives extension configs for a loaded plugin.
  private resultSubject: Subject<PluginExtensionConfigs<TMapType>>;
  protected logger: ExtensionsLog;
  // This is the subject that we expose.
  // (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
  protected registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;

  constructor(
    protected apps: AppPluginConfig[],
    options: {
      registrySubject?: ReplaySubject<RegistryType<TRegistryValue>>;
      initialState?: RegistryType<TRegistryValue>;
      log?: ExtensionsLog;
    }
  ) {
    this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
    this.logger = options.log ?? log;
    this.isReadOnly = false;

    // If the registry subject (observable) is provided, it means that all the registry updates are taken care of outside of this class -> it is read-only.
    if (options.registrySubject) {
      this.registrySubject = options.registrySubject;
      this.isReadOnly = true;
      return;
    }

    this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
    this.resultSubject
      .pipe(
        scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
        // Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
        startWith(options.initialState ?? {})
      )
      // Emitting the new registry to `this.registrySubject`
      .subscribe(this.registrySubject);
  }

  abstract mapToRegistry(
    registry: RegistryType<TRegistryValue>,
    item: PluginExtensionConfigs<TMapType>
  ): RegistryType<TRegistryValue>;

  register(result: PluginExtensionConfigs<TMapType>): void {
    if (this.isReadOnly) {
      throw new Error(MSG_CANNOT_REGISTER_READ_ONLY);
    }
    if (result.configs.length === 0) {
      return;
    }
    this.resultSubject.next(result);
  }

  asObservable(): Observable<RegistryType<TRegistryValue>> {
    return this.registrySubject.asObservable().pipe(map((state) => deepFreeze(state)));
  }

  asObservableSlice(selector: (state: RegistryType<TRegistryValue>) => TRegistryValue): Observable<TRegistryValue> {
    return this.registrySubject.asObservable().pipe(
      map((state) => selector(state)),
      distinctUntilChanged((prev, curr) => Object.is(prev, curr)),
      map((slice) => deepFreeze(slice))
    );
  }

  getState(): Promise<RegistryType<TRegistryValue>> {
    return firstValueFrom(this.asObservable());
  }
}
