import produce from "immer";

import {
  AppInterface,
  ServicesInterface,
  BaseService,
  DataAdapterInterface,
  DataAdapterContext,
  DataItemInterface,
  DataItemValueInterface,
  equals,
  DataItemHistoryOptionsInterface,
  DataItemIdentifierInterface,
  DataItemDimensionHistoryOptionsInterface,
  DataItemDimensionValueInterface,
} from "..";

type SubscriptionCallback = () => void;

export class DataService extends BaseService {
  private app: AppInterface;
  private services: ServicesInterface;
  private adapter: DataAdapterInterface;
  private context: DataAdapterContext;

  private itemStore = new Map<string, DataItemInterface>();
  private itemSubscriptions = new Map<string, Set<SubscriptionCallback>>();
  private itemListSubscriptions: Set<SubscriptionCallback> = new Set();
  private valueStore = new Map<string, DataItemValueInterface>();
  private valueSubscriptions = new Map<string, Set<SubscriptionCallback>>();

  constructor(app: AppInterface, adapter: DataAdapterInterface) {
    super();

    this.app = app;
    this.services = app.services;
    this.adapter = adapter;
    this.context = new DataAdapterContext(this, this.app);

    this.initAdapter(adapter, this.context, this.services);
  }

  private notifyDataSubscribers(
    subscribers: Set<SubscriptionCallback>,
    clear: boolean = false
  ): void {
    subscribers.forEach((callback) => {
      try {
        callback();
      } catch (error) {
        console.error("Error in subscription:", error);
      }
    });

    if (clear) {
      subscribers.clear();
    }
  }

  public subscribeItemList(callback: SubscriptionCallback) {
    this.itemListSubscriptions.add(callback);

    return () => {
      this.itemListSubscriptions.delete(callback);
    };
  }

  public subscribeItem(
    item: DataItemInterface,
    callback: SubscriptionCallback
  ) {
    const key = keyForItem(item);

    if (!this.itemSubscriptions.has(key)) {
      this.itemSubscriptions.set(key, new Set());
    }

    this.itemSubscriptions.get(key).add(callback);

    return () => {
      this.itemSubscriptions.get(key).delete(callback);
    };
  }

  public subscribeValue(
    item: DataItemInterface,
    callback: SubscriptionCallback
  ) {
    const key = keyForItem(item);

    if (!this.valueSubscriptions.has(key)) {
      this.valueSubscriptions.set(key, new Set());
    }

    this.valueSubscriptions.get(key).add(callback);

    return () => {
      this.valueSubscriptions.get(key).delete(callback);
    };
  }

  public async get(source: string, id: string): Promise<DataItemInterface> {
    return this._getOrThrowSync(source, id);
  }

  public _getOrThrowSync(source: string, id: string): DataItemInterface {
    const item = this.itemStore.get(keyForIdentifier([source, id]));

    if (!item) {
      throw new Error(`Item with id "${id}" not found`);
    }

    return item;
  }

  public async getValue(
    item: DataItemInterface
  ): Promise<DataItemValueInterface> {
    return this._getValueOrThrowSync(item);
  }

  public _getValueOrThrowSync(item: DataItemInterface): DataItemValueInterface {
    return this.valueStore.get(keyForItem(item));
  }

  public async list(): Promise<DataItemInterface[]> {
    return this._listOrThrowSync();
  }

  public _listOrThrowSync(): DataItemInterface[] {
    return Array.from(this.itemStore.values());
  }

  public async setItem(item: DataItemInterface): Promise<void> {
    const key = keyForItem(item);

    if (!this.itemSubscriptions.has(key)) {
      this.itemSubscriptions.set(key, new Set());
    }

    if (!this.itemStore.has(key) || equals(this.itemStore.get(key), item)) {
      this.itemStore.set(key, item);

      this.notifyDataSubscribers(this.itemListSubscriptions);
      this.notifyDataSubscribers(this.itemSubscriptions.get(key));
    }
  }

  public async setItems(items: DataItemInterface[]): Promise<void> {
    for (const item of items) {
      const key = keyForItem(item);

      if (!this.itemSubscriptions.has(key)) {
        this.itemSubscriptions.set(key, new Set());
      }

      if (!this.itemStore.has(key) || !equals(this.itemStore.get(key), item)) {
        this.itemStore.set(key, item);

        this.notifyDataSubscribers(this.itemSubscriptions.get(key));
      }
    }

    this.notifyDataSubscribers(this.itemListSubscriptions);
  }

  public async setValue(
    item: DataItemInterface,
    value: DataItemValueInterface
  ): Promise<void> {
    const key = keyForItem(item);

    if (!this.valueSubscriptions.has(key)) {
      this.valueSubscriptions.set(key, new Set());
    }

    if (!this.valueStore.has(key) || !equals(this.valueStore.get(key), value)) {
      this.valueStore.set(key, value);

      this.notifyDataSubscribers(this.valueSubscriptions.get(key));
    }
  }

  public async removeItem(item: DataItemInterface): Promise<void> {
    const key = keyForItem(item);

    this.itemStore.delete(key);
    this.valueStore.delete(key);

    this.notifyDataSubscribers(this.itemListSubscriptions);
    this.notifyDataSubscribers(this.itemSubscriptions.get(key));
    this.notifyDataSubscribers(this.valueSubscriptions.get(key));
  }

  public async clear() {
    this.setLoading(true);

    // clear stores
    this.itemStore.clear();
    this.valueStore.clear();

    this.notifyDataSubscribers(this.itemListSubscriptions);

    Array.from(this.itemSubscriptions.keys()).forEach((key) => {
      this.notifyDataSubscribers(this.itemSubscriptions.get(key));
    });

    Array.from(this.valueSubscriptions.keys()).forEach((key) => {
      this.notifyDataSubscribers(this.valueSubscriptions.get(key));
    });
  }

  public async fetchValues(
    item: DataItemInterface,
    options: DataItemHistoryOptionsInterface
  ): Promise<DataItemValueInterface[]> {
    await this.wait();

    return await this.adapter.fetchValues(item, options);
  }

  public async fetchValuesMultiItem(
    items: DataItemInterface[],
    options: DataItemHistoryOptionsInterface
  ): Promise<[DataItemInterface, DataItemValueInterface[]][]> {
    await this.wait();

    const historyRequests = items.map((item) =>
      this.adapter.fetchValues(item, options)
    );

    const histories = await Promise.all(historyRequests);

    const resultEntries = items.map(
      (item, i) =>
        [item, histories[i]] as [DataItemInterface, DataItemValueInterface[]]
    );

    return resultEntries;
  }

  public async fetchDimensionValues(
    item: DataItemInterface,
    dimension: number,
    options: DataItemDimensionHistoryOptionsInterface
  ): Promise<DataItemDimensionValueInterface[]> {
    await this.wait();

    return await this.adapter.fetchDimensionValues(item, dimension, options);
  }

  public async fetchDimensionValuesMultiItem(
    items: [DataItemInterface, number][],
    options: DataItemHistoryOptionsInterface
  ): Promise<[DataItemInterface, number, DataItemDimensionValueInterface[]][]> {
    await this.wait();

    const historyRequests = items.map(([item, dimension]) =>
      this.adapter.fetchDimensionValues(item, dimension, options)
    );

    const histories = await Promise.all(historyRequests);

    const resultEntries = items.map(
      ([item, dimension], i) =>
        [item, dimension, histories[i]] as [
          DataItemInterface,
          number,
          DataItemDimensionValueInterface[]
        ]
    );

    return resultEntries;
  }

  public async update(item: DataItemInterface): Promise<void> {
    return await this.adapter.update(item);
  }
}

function keyForIdentifier(item: DataItemIdentifierInterface): string {
  return JSON.stringify(item);
}

function keyForItem(item: DataItemInterface): string {
  return JSON.stringify([item.source, item.id]);
}
