/*
 * Copyright (C) 2023 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
/* eslint-disable unicorn/no-null */
import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core';
import {
  Bounds,
  SCDateSeries,
  SCISO8601Date,
  SCISO8601Duration,
  SCSearchFilter,
  SCThingType,
  SCUuid,
} from '@openstapps/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {DataProvider} from '../data/data.provider';
import {map} from 'rxjs/operators';
import {DateFormatPipe, DurationPipe} from 'ngx-moment';
import {pick} from '@openstapps/collection-utils';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/**
 *
 */
export function toDateSeriesRelevantData(dateSeries: SCDateSeries): DateSeriesRelevantData {
  return pick(dateSeries, dateSeriesRelevantKeys);
}

export type DateSeriesRelevantKeys = 'uid' | 'dates' | 'exceptions' | 'repeatFrequency' | 'duration';

export const dateSeriesRelevantKeys: Array<DateSeriesRelevantKeys> = [
  'uid',
  'dates',
  'exceptions',
  'repeatFrequency',
  'duration',
];

export const formatRelevantKeys: {
  [key in DateSeriesRelevantKeys]: (
    value: SCDateSeries[key],
    dateFormatter: DateFormatPipe,
    durationFormatter: DurationPipe,
  ) => string;
} = {
  uid: value => value,
  dates: (value, dateFormatter) => `[${value.map(it => dateFormatter.transform(it)).join(', ')}]`,
  exceptions: (value, dateFormatter) => `[${value?.map(it => dateFormatter.transform(it)).join(', ') ?? ''}]`,
  repeatFrequency: (value, _, durationFormatter) => durationFormatter.transform(value),
  duration: (value, _, durationFormatter) => durationFormatter.transform(value),
};

export type DateSeriesRelevantData = Pick<SCDateSeries, DateSeriesRelevantKeys>;

/**
 * Provider for app settings
 */
@Injectable()
export class ScheduleProvider implements OnDestroy {
  // tslint:disable:prefer-function-over-method

  private static partialEventsStorageKey = 'schedule::partial_events';

  private _partialEvents$?: BehaviorSubject<DateSeriesRelevantData[]>;

  destroy$ = inject(DestroyRef);

  constructor(private readonly dataProvider: DataProvider) {
    window.addEventListener('storage', this.storageListener);
  }

  /**
   * Push one or more values to local storage
   */
  private static get<T>(key: string): T[] {
    const item = localStorage.getItem(key);
    if (item == undefined) {
      return [];
    }

    return JSON.parse(item) as T[];
  }

  /**
   * Push one or more values to local storage
   */
  private static set<T>(key: string, item: T[]) {
    const newValue = JSON.stringify(item);
    // prevent feedback loop from storageEvent -> _uuids$.next() -> set -> storageEvent
    if (newValue !== localStorage.getItem(key)) {
      localStorage.setItem(key, newValue);
    }
  }

  public async restore(uuids: SCUuid[]): Promise<SCDateSeries[] | undefined> {
    if (uuids.length === 0) {
      return undefined;
    }
    const dateSeries = (await this.getDateSeries(uuids)).dates;

    this._partialEvents$?.next(dateSeries.map(toDateSeriesRelevantData));

    return dateSeries;
  }

  /**
   * TODO
   */
  public get uuids$(): Observable<SCUuid[]> {
    return this.partialEvents$.pipe(map(events => events.map(it => it.uid)));
  }

  public get partialEvents$(): BehaviorSubject<DateSeriesRelevantData[]> {
    if (!this._partialEvents$) {
      const data = ScheduleProvider.get<DateSeriesRelevantData>(ScheduleProvider.partialEventsStorageKey);

      this._partialEvents$ = new BehaviorSubject(data ?? []);
      this._partialEvents$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(result => {
        ScheduleProvider.set(ScheduleProvider.partialEventsStorageKey, result);
      });
    }

    return this._partialEvents$;
  }

  /**
   * What to do when local storage updates
   */
  private storageEventHandler(event: StorageEvent) {
    if (
      event.newValue &&
      event.storageArea === localStorage &&
      event.key === ScheduleProvider.partialEventsStorageKey
    ) {
      this._partialEvents$?.next(JSON.parse(event.newValue));
    }
  }

  /**
   * Listen to updates in local storage
   */
  private storageListener = this.storageEventHandler.bind(this);

  /**
   * Load Date Series
   */
  async getDateSeries(
    uuids: SCUuid[],
    frequencies?: Array<SCISO8601Duration>,
    from?: SCISO8601Date | 'now',
    to?: SCISO8601Date | 'now',
  ): Promise<{
    dates: SCDateSeries[];
    min: SCISO8601Date;
    max: SCISO8601Date;
  }> {
    if (uuids.length === 0) {
      return {
        dates: [],
        min: '',
        max: '',
      };
    }

    const filters: SCSearchFilter[] = [
      {
        arguments: {
          field: 'type',
          value: SCThingType.DateSeries,
        },
        type: 'value',
      },
      {
        arguments: {
          filters: uuids.map(uid => ({
            arguments: {
              field: 'uid',
              value: uid,
            },
            type: 'value',
          })),
          operation: 'or',
        },
        type: 'boolean',
      },
    ];

    if (frequencies) {
      filters.push({
        arguments: {
          filters: frequencies.map(frequency => ({
            arguments: {
              field: 'repeatFrequency',
              value: frequency,
            },
            type: 'value',
          })),
          operation: 'or',
        },
        type: 'boolean',
      });
    }

    if (from || to) {
      const bounds: Bounds<string> = {};
      if (from) {
        bounds.lowerBound = {
          limit: from,
          mode: 'inclusive',
        };
      }
      if (to) {
        bounds.upperBound = {
          limit: to,
          mode: 'inclusive',
        };
      }
      filters.push({
        arguments: {
          field: 'dates',
          bounds: bounds,
        },
        type: 'date range',
      });
    }

    const result = await this.dataProvider.search({
      filter: {
        arguments: {
          filters: filters,
          operation: 'and',
        },
        type: 'boolean',
      },
      size: 50,
    });

    return {
      dates: result.data as SCDateSeries[],
      // TODO: https://gitlab.com/openstapps/backend/-/issues/100
      min: new Date(2021, 11, 1).toISOString(),
      max: new Date(2022, 1, 24).toISOString(),
    };
  }

  /**
   * TODO
   */
  ngOnDestroy(): void {
    window.removeEventListener('storage', this.storageListener);
  }
}
