import { inject, Injectable } from '@angular/core';
import { EMPTY, fromEvent, merge, Observable, of, ReplaySubject, timer } from 'rxjs';
import {
  auditTime,
  concatMap,
  debounceTime,
  distinctUntilChanged,
  filter,
  first,
  map,
  mapTo,
  shareReplay,
  skip,
  startWith,
  switchMap,
  take,
  tap,
  throttleTime,
} from 'rxjs/operators';
import { GlobalConfigService } from '../../features/config/global-config.service';
import { isOnline$ } from '../../util/is-online';
import {
  SYNC_BEFORE_GOING_TO_SLEEP_THROTTLE_TIME,
  SYNC_DEFAULT_AUDIT_TIME,
  SYNC_MIN_INTERVAL,
} from './sync.const';
import { IdleService } from '../../features/idle/idle.service';
import { IS_ELECTRON } from '../../app.constants';
import { GlobalConfigState } from '../../features/config/global-config.model';
import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
import { androidInterface } from '../../features/android/android-interface';
import { ipcResume$, ipcSuspend$ } from '../../core/ipc-events';
import { IS_TOUCH_PRIMARY } from '../../util/is-mouse-primary';
import { DataInitStateService } from '../../core/data-init/data-init-state.service';
import { SyncLog } from '../../core/log';
import { HydrationStateService } from '../../op-log/apply/hydration-state.service';
import { SyncWrapperService } from './sync-wrapper.service';
import { SyncProviderId } from '../../op-log/sync-exports';

const MAX_WAIT_FOR_INITIAL_SYNC = 8000;
/** 15 minutes in milliseconds - throttle time for user activity sync checks */
const USER_ACTIVITY_SYNC_THROTTLE_TIME = 15 * 60 * 1000;

@Injectable({
  providedIn: 'root',
})
export class SyncTriggerService {
  private readonly _globalConfigService = inject(GlobalConfigService);
  private readonly _dataInitStateService = inject(DataInitStateService);
  private readonly _idleService = inject(IdleService);
  private readonly _syncWrapperService = inject(SyncWrapperService);
  private readonly _hydrationState = inject(HydrationStateService);

  constructor() {
    // When sync is disabled, set initialSyncDone immediately so UI shows
    // and day-change effects can run without waiting for a sync that will never happen.
    this._isInitialSyncEnabled$.pipe(first()).subscribe((isActive) => {
      if (!isActive) {
        this.setInitialSyncDone(true);
      }
    });

    // Open the sync window directly on every resume source, bypassing the
    // gated `getSyncTrigger$()` chain (which is only subscribed once
    // dataInitState + config are ready). This handles the cold-start edge
    // case where a resume arrives before the trigger pipeline is wired.
    // The failsafe in `openSyncWindow()` cleans up if no sync follows.
    if (IS_ANDROID_WEB_VIEW) {
      androidInterface.onResume$.subscribe(() => this._hydrationState.openSyncWindow());
    }
    if (IS_ELECTRON) {
      ipcResume$.subscribe(() => this._hydrationState.openSyncWindow());
    }
    // Open synchronously on visibilitychange — bridged onResume$ on Android
    // arrives ~100ms later, after the DAY_CHANGE → TODAY_TAG repair cascade.
    fromEvent(document, 'visibilitychange')
      .pipe(filter(() => document.visibilityState === 'visible'))
      .subscribe(() => this._hydrationState.openSyncWindow());
  }

  // Note: This was previously connected to PFAPI's onLocalMetaUpdate$, which was a no-op.
  // For file-based sync, this doesn't matter as sync is immediate-upload based.
  // For SuperSync, operations are uploaded immediately via ImmediateUploadService.
  private _onUpdateLocalDataTrigger$: Observable<unknown> = of(null);

  // IMMEDIATE TRIGGERS
  // ----------------------
  private _mouseMoveAfterIdleOrUserInteractionFallbackTrigger$: Observable<
    string | never
  > = this._globalConfigService.idle$.pipe(
    switchMap((idleCfg) =>
      idleCfg.isEnableIdleTimeTracking
        ? // idle should be a good indicator for remote data changes
          this._idleService.isIdle$.pipe(
            distinctUntilChanged(),
            switchMap((isIdle) =>
              isIdle
                ? fromEvent(window, 'mousemove').pipe(
                    // we throttle this to prevent lots of updates, but
                    // but also cover the case when the user doesn't interact with the idle dialog
                    throttleTime(60 * 1000),
                    mapTo('I_MOUSE_MOVE_AFTER_IDLE_THROTTLED'),
                  )
                : EMPTY,
            ),
          )
        : // FALLBACK we check if there was any kind of user interaction
          // (otherwise sync might never be checked if there are no local data changes)
          // NOTE: visibilitychange is handled separately by _visibilityHiddenTrigger$ —
          // not throttled, so backgrounding always attempts a sync.
          IS_TOUCH_PRIMARY
          ? fromEvent(window, 'touchstart').pipe(
              mapTo('I_TOUCH_ACTIVITY'),
              throttleTime(USER_ACTIVITY_SYNC_THROTTLE_TIME),
            )
          : fromEvent(window, 'focus').pipe(
              mapTo('I_FOCUS_THROTTLED'),
              throttleTime(USER_ACTIVITY_SYNC_THROTTLE_TIME),
            ),
    ),
  );

  private _onIdleTrigger$: Observable<string | never> = this._idleService.isIdle$.pipe(
    distinctUntilChanged(),
    switchMap((isIdle) =>
      isIdle
        ? // NOTE: wait for a second for all possible data changes and disabling timers to take place
          timer(1000).pipe(mapTo('I_ON_IDLE'))
        : EMPTY,
    ),
  );

  private _onElectronResumeTrigger$: Observable<string | never> = IS_ELECTRON
    ? ipcResume$.pipe(
        // because ipcEvents live forever
        mapTo('I_IPC_RESUME'),
      )
    : EMPTY;
  private _beforeGoingToSleepTriggers$: Observable<string | never> = IS_ELECTRON
    ? ipcSuspend$.pipe(
        // because ipcEvents live forever
        mapTo('I_IPC_SUSPEND'),
        throttleTime(SYNC_BEFORE_GOING_TO_SLEEP_THROTTLE_TIME),
      )
    : EMPTY;

  private _isOnlineTrigger$: Observable<string> = isOnline$.pipe(
    // skip initial online which always fires on page load
    skip(1),
    filter((isOnline) => isOnline),
    mapTo('I_IS_ONLINE'),
  );

  // Fires when the page becomes hidden (tab switch, app backgrounding, close).
  // Not throttled — a "last chance before close" trigger should always attempt a sync.
  // Electron uses execBeforeCloseService (IPC-level blocking close) instead.
  private _visibilityHiddenTrigger$: Observable<string | never> = !IS_ELECTRON
    ? fromEvent(document, 'visibilitychange').pipe(
        filter(() => document.visibilityState === 'hidden'),
        mapTo('I_VISIBILITY_HIDDEN'),
      )
    : EMPTY;

  // OTHER INITIAL SYNC STUFF
  // ------------------------
  private _isInitialSyncEnabled$: Observable<boolean> =
    this._dataInitStateService.isAllDataLoadedInitially$.pipe(
      switchMap(() => this._globalConfigService.cfg$),
      map((cfg: GlobalConfigState) => cfg.sync.isEnabled),
      distinctUntilChanged(),
    );

  // keep it super simple for now
  private _isInitialSyncDoneManual$: ReplaySubject<boolean> = new ReplaySubject<boolean>(
    1,
  );
  private _isInitialSyncDoneSync = false;

  private _isInitialSyncDone$: Observable<boolean> = this._isInitialSyncEnabled$.pipe(
    switchMap((isActive) => {
      if (!isActive) {
        return of(true);
      }
      // SuperSync has data locally - no need to wait for initial sync for UI display
      return this._syncWrapperService.syncProviderId$.pipe(
        take(1),
        switchMap((providerId) =>
          providerId === SyncProviderId.SuperSync
            ? of(true)
            : this._isInitialSyncDoneManual$.asObservable(),
        ),
      );
    }),
  );
  private _afterInitialSyncDoneAndDataLoadedInitially$: Observable<boolean> =
    this._isInitialSyncDone$.pipe(
      filter((isDone) => isDone),
      take(1),
      // should normally be already loaded, but if there is NO initial sync we need to wait here
      concatMap(() => this._dataInitStateService.isAllDataLoadedInitially$),
    );

  // NOTE: can be called multiple times apparently
  afterInitialSyncDoneAndDataLoadedInitially$: Observable<boolean> = merge(
    this._afterInitialSyncDoneAndDataLoadedInitially$,
    timer(MAX_WAIT_FOR_INITIAL_SYNC).pipe(
      tap(() => this.setInitialSyncDone(true)),
      mapTo(true),
    ),
  ).pipe(first(), shareReplay(1));

  /**
   * Similar to afterInitialSyncDoneAndDataLoadedInitially$, but ALWAYS waits for
   * the actual initial sync to complete - even for SuperSync.
   *
   * Use this for operations that must have fully synchronized data before running,
   * like repeatable task creation, to prevent duplicate tasks across clients.
   */
  afterInitialSyncDoneStrict$: Observable<boolean> = this._isInitialSyncEnabled$.pipe(
    switchMap((isActive) => {
      if (!isActive) {
        return of(true);
      }
      return merge(
        this._isInitialSyncDoneManual$.asObservable().pipe(filter((isDone) => isDone)),
        timer(MAX_WAIT_FOR_INITIAL_SYNC).pipe(
          tap(() => this.setInitialSyncDone(true)),
          mapTo(true),
        ),
      ).pipe(first());
    }),
    concatMap(() => this._dataInitStateService.isAllDataLoadedInitially$),
    first(),
    shareReplay(1),
  );

  getSyncTrigger$(
    syncInterval: number = SYNC_DEFAULT_AUDIT_TIME,
    useIntervalTimer = false,
  ): Observable<unknown> {
    const _immediateSyncTrigger$: Observable<string> = IS_ANDROID_WEB_VIEW
      ? // ANDROID ONLY
        merge(
          // to update in background for widget
          androidInterface.isInBackground$.pipe(
            switchMap((isInBackground) =>
              isInBackground
                ? timer(syncInterval, syncInterval).pipe(
                    mapTo('I_MOBILE_ONLY_BACKGROUND_TIMER'),
                  )
                : EMPTY,
            ),
          ),
          androidInterface.onResume$.pipe(throttleTime(10000), mapTo('I_RESUME_APP')),
          androidInterface.onPause$.pipe(throttleTime(10000), mapTo('I_PAUSE_APP')),
          this._isOnlineTrigger$,
        )
      : // EVERYTHING ELSE
        merge(
          this._mouseMoveAfterIdleOrUserInteractionFallbackTrigger$,
          this._beforeGoingToSleepTriggers$,
          this._isOnlineTrigger$,
          this._onIdleTrigger$,
          this._onElectronResumeTrigger$,
          this._visibilityHiddenTrigger$,
          // Periodic interval timer: fires every syncInterval ms to detect external file
          // changes (e.g. Syncthing on Linux) even without user activity.
          // Only for file-based providers — SuperSync uses WebSocket push and doesn't need polling.
          // Fixes: Linux auto-sync ignoring interval (#4783), Dropbox sync gaps (#7144)
          ...(useIntervalTimer
            ? [timer(syncInterval, syncInterval).pipe(mapTo('I_INTERVAL_TIMER'))]
            : []),
        );
    return merge(
      // once immediately
      _immediateSyncTrigger$.pipe(
        tap((v) => {
          SyncLog.log('immediate sync trigger', v);
          // Open BEFORE the downstream debounceTime(100) so the resume → DAY_CHANGE
          // → TODAY_TAG repair cascade (which fires inside that 100ms) sees the
          // window already open and is suppressed by skipDuringSyncWindow().
          // Failsafe in openSyncWindow() handles triggers that get debounced/
          // throttled out before reaching SyncWrapperService.sync().
          this._hydrationState.openSyncWindow();
        }),
      ),

      // and once we reset the sync interval for all other triggers
      // we do this to reset the audit time to avoid sync checks in short succession
      _immediateSyncTrigger$.pipe(
        // NOTE: startWith needs to come before switchMap!
        // NOTE2: we use startWith, since we want to start listening to the onUpdateLocalDataTrigger right away
        startWith('INITIAL_TIMER_TRIGGER'),
        switchMap(() =>
          // NOTE: interval changes are only ever executed, if local data was changed
          this._onUpdateLocalDataTrigger$.pipe(
            // tap((ev) => Log.log('__trigger_sync__', ev.appDataKey, ev)),
            // tap((ev) => Log.log('__trigger_sync__', 'I_ON_UPDATE_LOCAL_DATA', ev)),
            auditTime(Math.max(syncInterval, SYNC_MIN_INTERVAL)),
            // tap((ev) => alert('__trigger_sync after auditTime__')),
          ),
        ),
      ),
    ).pipe(debounceTime(100));
  }

  setInitialSyncDone(val: boolean): void {
    this._isInitialSyncDoneSync = val;
    this._isInitialSyncDoneManual$.next(val);
  }

  /**
   * Synchronous getter for initial sync done state.
   * Used by operators like skipDuringSyncWindow() that need
   * to check state synchronously in filter callbacks.
   */
  isInitialSyncDoneSync(): boolean {
    return this._isInitialSyncDoneSync;
  }
}
