/*
 * 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/>.
 */
import {DestroyRef, inject, Injectable} from '@angular/core';
import {
  DateSeriesRelevantData,
  dateSeriesRelevantKeys,
  formatRelevantKeys,
  ScheduleProvider,
} from '../../calendar/schedule.provider';
import {SCDateSeries, SCThingType, SCUuid} from '@openstapps/core';
import {LocalNotifications} from '@capacitor/local-notifications';
import {ThingTranslateService} from '../../../translation/thing-translate.service';
import {DateFormatPipe, DurationPipe} from 'ngx-moment';
import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
import {StorageProvider} from '../../storage/storage.provider';
import {CalendarService} from '../../calendar/calendar.service';
import {toICal} from '../../calendar/ical/ical';
import {ChangesOf} from './changes';
import {hashStringToInt} from './hash';
import {
  CALENDAR_NOTIFICATIONS_ENABLED_KEY,
  CALENDAR_SYNC_ENABLED_KEY,
  getCalendarSetting,
} from '../../settings/page/calendar-sync-settings-keys';
import {filter} from 'rxjs/operators';
import {Capacitor} from '@capacitor/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

@Injectable()
export class ScheduleSyncService {
  destroy$ = inject(DestroyRef);

  constructor(
    private scheduleProvider: ScheduleProvider,
    private storageProvider: StorageProvider,
    private translator: ThingTranslateService,
    private dateFormatPipe: DateFormatPipe,
    private durationFormatPipe: DurationPipe,
    private calendar: CalendarService,
  ) {}

  init() {
    this.scheduleProvider.uuids$
      .pipe(
        takeUntilDestroyed(this.destroy$),
        filter(uuids => uuids?.length > 0),
      )
      .subscribe(uuids => {
        this.uuids = uuids;
        void this.syncNativeCalendar();
      });
  }

  uuids: SCUuid[];

  private async isSyncEnabled(): Promise<boolean> {
    return getCalendarSetting(this.storageProvider, CALENDAR_SYNC_ENABLED_KEY);
  }

  private async isNotificationsEnabled(): Promise<boolean> {
    return getCalendarSetting(this.storageProvider, CALENDAR_NOTIFICATIONS_ENABLED_KEY);
  }

  async enable() {
    if (!Capacitor.isNativePlatform()) return;

    await BackgroundFetch.stop();

    if (
      [this.isSyncEnabled.bind(this), this.isNotificationsEnabled.bind(this)].some(async it => await it())
    ) {
      const status = await BackgroundFetch.configure(
        {
          minimumFetchInterval: 15,
          requiredNetworkType: 1,
        },
        async taskId => {
          await Promise.all([this.postDifferencesNotification(), this.syncNativeCalendar()]);

          BackgroundFetch.finish(taskId);
        },
        async taskId => {
          console.log('[BackgroundFetch] TIMEOUT:', taskId);
          BackgroundFetch.finish(taskId);
        },
      );

      if (status === BackgroundFetch.STATUS_AVAILABLE) {
        console.info('Starting background fetch.');

        await BackgroundFetch.start();
      } else {
        if (status === BackgroundFetch.STATUS_DENIED) {
          console.error(
            'The user explicitly disabled background behavior for this app or for the whole system.',
          );
        } else if (status === BackgroundFetch.STATUS_RESTRICTED) {
          console.error('Background updates are unavailable and the user cannot enable them again.');
        }
      }
    }
  }

  async getDifferences(): Promise<ChangesOf<SCDateSeries, DateSeriesRelevantData>[]> {
    const partialEvents = this.scheduleProvider.partialEvents$.getValue();

    const result = (await this.scheduleProvider.getDateSeries(partialEvents.map(it => it.uid))).dates;

    return result
      .map(it => ({
        new: it,
        old: partialEvents.find(partialEvent => partialEvent.uid === it.uid),
      }))
      .map(it => ({
        ...it,
        changes: it.old
          ? (Object.keys(it.old) as Array<keyof DateSeriesRelevantData>).filter(
              key =>
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                JSON.stringify(it.old![key]) !== JSON.stringify(it.new[key]),
            )
          : dateSeriesRelevantKeys,
      }));
  }

  private formatChanges(changes: ChangesOf<SCDateSeries, DateSeriesRelevantData>): string[] {
    return changes.changes.map(
      change =>
        `${this.translator.translator.translatedPropertyNames<SCDateSeries>(SCThingType.DateSeries)?.[
          change
        ]}: ${formatRelevantKeys[change](
          changes.new[change] as never,
          this.dateFormatPipe,
          this.durationFormatPipe,
        )}`,
    );
  }

  async syncNativeCalendar() {
    if (!(await this.isSyncEnabled())) return;

    const dateSeries = (await this.scheduleProvider.getDateSeries(this.uuids)).dates;

    const events = dateSeries.flatMap(event =>
      toICal(event, this.translator.translator, {
        allowRRuleExceptions: false,
        excludeCancelledEvents: true,
      }),
    );

    return this.calendar.syncEvents(events);
  }

  async postDifferencesNotification() {
    if (!(await this.isNotificationsEnabled())) return;

    const differences = (await this.getDifferences()).filter(it => it.changes.length > 0);
    if (differences.length === 0) return;

    if (Capacitor.isNativePlatform()) {
      await LocalNotifications.schedule({
        notifications: differences.map(it => ({
          title: it.new.event.name,
          body: this.formatChanges(it).join('\n'),
          id: hashStringToInt(it.new.uid),
        })),
      });
    } else {
      // TODO: Implement desktop notifications
    }
  }
}
