import { effect, inject, Injectable, Injector } from '@angular/core';
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
import { TranslateService } from '@ngx-translate/core';
import { LocalBackupService } from '../../imex/local-backup/local-backup.service';
import { GlobalConfigService } from '../../features/config/global-config.service';
import { SnackService } from '../snack/snack.service';
import { PluginService } from '../../plugins/plugin.service';
import { SyncWrapperService } from '../../imex/sync/sync-wrapper.service';
import { BannerService } from '../banner/banner.service';
import { UiHelperService } from '../../features/ui-helper/ui-helper.service';
import { ChromeExtensionInterfaceService } from '../chrome-extension-interface/chrome-extension-interface.service';
import { ProjectService } from '../../features/project/project.service';
import { IS_ELECTRON } from '../../app.constants';
import { Log } from '../log';
import { T } from '../../t.const';
import { OperationLogStoreService } from '../../op-log/persistence/operation-log-store.service';
import { LegacyPfDbService } from '../persistence/legacy-pf-db.service';
import { BannerId } from '../banner/banner.model';
import { isOnline$ } from '../../util/is-online';
import { LS } from '../persistence/storage-keys.const';
import { RatePromptService } from '../../features/dialog-please-rate/rate-prompt.service';
import { map, switchMap, take } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
import { Store } from '@ngrx/store';
import { selectSyncConfig } from '../../features/config/store/global-config.reducer';
import { selectEnabledIssueProviders } from '../../features/issue/store/issue-provider.selectors';
import { SyncProviderId } from '../../op-log/sync-providers/provider.const';
import { IPC } from '../../../../electron/shared-with-frontend/ipc-events.const';
import { IpcRendererEvent } from 'electron';
import { environment } from '../../../environments/environment';
import { TrackingReminderService } from '../../features/tracking-reminder/tracking-reminder.service';
import { CapacitorPlatformService } from '../platform/capacitor-platform.service';
import { alertDialog } from '../../util/native-dialogs';
import { DataInitStateService } from '../data-init/data-init-state.service';
import { OnboardingHintService } from '../../features/onboarding/onboarding-hint.service';
import { LocalRestApiHandlerService } from '../electron/local-rest-api-handler.service';
import { CustomThemeService } from '../theme/custom-theme.service';
import { UpdateCheckService } from '../update-check/update-check.service';

const w = window as Window & { productivityTips?: string[][]; randomIndex?: number };

/** Delay before running deferred initialization tasks (plugins, storage checks, etc.) */
const DEFERRED_INIT_DELAY_MS = 1000;

/**
 * Cap on how long the persisted theme is allowed to block startup. Built-ins
 * finish in <1 ms (no IDB read), normal user-theme reads land in 15-120 ms.
 * Only a stalled IDB hits this timeout; we then fall through to default
 * rendering so the splash screen can't hang forever on a corrupted store.
 */
const APPLY_THEME_TIMEOUT_MS = 500;

@Injectable({
  providedIn: 'root',
})
export class StartupService {
  private _imexMetaService = inject(ImexViewService);
  private _translateService = inject(TranslateService);
  private _localBackupService = inject(LocalBackupService);
  private _globalConfigService = inject(GlobalConfigService);
  private _snackService = inject(SnackService);
  private _ratePromptService = inject(RatePromptService);
  private _pluginService = inject(PluginService);
  private _syncWrapperService = inject(SyncWrapperService);
  private _bannerService = inject(BannerService);
  private _uiHelperService = inject(UiHelperService);
  private _chromeExtensionInterfaceService = inject(ChromeExtensionInterfaceService);
  private _projectService = inject(ProjectService);
  private _trackingReminderService = inject(TrackingReminderService);
  private _updateCheckService = inject(UpdateCheckService);
  private _opLogStore = inject(OperationLogStoreService);
  private _legacyPfDb = inject(LegacyPfDbService);
  private _store = inject(Store);
  private _platformService = inject(CapacitorPlatformService);
  private _dataInitStateService = inject(DataInitStateService);
  private _injector = inject(Injector);
  private _customThemeService = inject(CustomThemeService);

  constructor() {
    // Initialize electron error handler in an effect
    if (IS_ELECTRON) {
      effect(() => {
        window.ea.on(IPC.ERROR, (ev: IpcRendererEvent, ...args: unknown[]) => {
          const data = args[0] as {
            error: unknown;
            stack: unknown;
            errorStr: string | unknown;
          };
          const errMsg =
            typeof data.errorStr === 'string' ? data.errorStr : ' INVALID ERROR MSG :( ';

          this._snackService.open({
            msg: errMsg,
            type: 'ERROR',
            isSkipTranslate: true,
          });
          Log.err(data);
        });
      });
    }
  }

  async init(): Promise<void> {
    // Skip single instance check for native mobile apps and Electron
    if (!this._platformService.isNative && !IS_ELECTRON) {
      const isSingle = await this._checkIsSingleInstance();
      if (!isSingle) {
        this._showMultiInstanceBlocker();
        return;
      }
    }

    this._initBackups();
    this._requestPersistence();

    // Apply the persisted custom theme before the deferred init / Electron
    // ready notification, so the page doesn't briefly flash the default
    // stylesheet. Worst-case adds one IDB read for user themes — guarded by
    // a hard timeout so a corrupted/blocked IDB can't hang the splash.
    try {
      await Promise.race([
        this._customThemeService.applyActiveTheme(),
        new Promise<void>((resolve) => setTimeout(resolve, APPLY_THEME_TIMEOUT_MS)),
      ]);
    } catch (err) {
      Log.err({ stage: 'apply-active-theme', error: (err as Error).message });
    }

    // deferred init
    window.setTimeout(async () => {
      this._trackingReminderService.init();
      this._updateCheckService.init();
      this._checkAvailableStorage();
      this._initOfflineBanner();

      const miscCfg = this._globalConfigService.misc();

      // One-time migration for users syncing from a device that still
      // wrote the theme into `globalConfig.misc.customTheme`. Brief flash
      // of default → preferred is acceptable and only happens once.
      // Wrapped because a failure here must not skip the productivity-tip
      // snack and `_initPlugins` further down in this deferred-init body.
      if (miscCfg?.customTheme) {
        try {
          await this._customThemeService.migrateLegacyCustomTheme(miscCfg.customTheme);
        } catch (err) {
          Log.err({
            stage: 'migrate-legacy-custom-theme',
            error: (err as Error).message,
          });
        }
      }

      if (miscCfg?.isShowProductivityTipLonger && !this._isTourLikelyToBeShown()) {
        if (w.productivityTips && w.randomIndex !== undefined) {
          this._snackService.open({
            ico: 'lightbulb',
            config: {
              duration: 16000,
            },
            msg:
              '<strong>' +
              w.productivityTips[w.randomIndex][0] +
              ':</strong> ' +
              w.productivityTips[w.randomIndex][1],
          });
        }
      }

      this._ratePromptService.init();
      await this._initPlugins();
    }, DEFERRED_INIT_DELAY_MS);

    if (IS_ELECTRON) {
      this._injector.get(LocalRestApiHandlerService).init();

      window.ea.on(IPC.TRANSFER_SETTINGS_REQUESTED, () =>
        this._sendCurrentSettingsToElectronAfterDataLoad(),
      );
      this._sendCurrentSettingsToElectronAfterDataLoad();

      window.ea.informAboutAppReady();
      this._uiHelperService.initElectron();
    } else {
      // WEB VERSION
      window.addEventListener('beforeunload', (e) => {
        const gCfg = this._globalConfigService.cfg();
        if (!gCfg) {
          throw new Error();
        }
        if (
          gCfg.misc.isConfirmBeforeExit ||
          this._syncWrapperService.isSyncInProgressSync()
        ) {
          e.preventDefault();
          e.returnValue = '';
        }
      });

      // Chrome extension only works in web browser, not native mobile apps
      if (!this._platformService.isNative) {
        this._chromeExtensionInterfaceService.init();
      }
    }
  }

  private _sendCurrentSettingsToElectronAfterDataLoad(): void {
    this._dataInitStateService.isAllDataLoadedInitially$
      .pipe(
        take(1),
        switchMap(() => this._globalConfigService.cfg$.pipe(take(1))),
      )
      .subscribe((cfg) => window.ea.sendAppSettingsToElectron(cfg));
  }

  private async _initBackups(): Promise<void> {
    // if completely fresh instance check for local backups
    // Local backups are available on Electron and native mobile (iOS/Android)
    if (IS_ELECTRON || this._platformService.isNative) {
      const stateCache = await this._opLogStore.loadStateCache();
      // If no state cache exists, check if this is truly a fresh instance
      // or if there's legacy v16 data waiting to be migrated
      if (!stateCache) {
        // #7901: only consider a backup restore when the op-log is ALSO empty. A
        // null state cache alone just means "no snapshot saved yet" — the op-log
        // may still hold real operations the hydrator is concurrently replaying.
        // Restoring then would be unnecessary (the hydrator loads that data) and
        // would race the hydrator's replay against importCompleteBackup's
        // destructive op-log replacement. Gating on an empty op-log restricts
        // restore to a genuinely blank store (fresh install / evicted storage),
        // where the hydrator has nothing to replay.
        const lastSeq = await this._opLogStore.getLastSeq();
        if (lastSeq === 0) {
          // Check for legacy data - if it exists, don't show restore dialog
          // The migration service will handle the legacy data
          let hasLegacyData = false;
          try {
            hasLegacyData = await this._legacyPfDb.hasUsableEntityData();
          } catch (e) {
            // If legacy check fails, it means the database exists but can't be read
            // The migration service will handle this error properly
            Log.warn(
              'StartupService: Legacy data check failed, skipping backup prompt',
              e,
            );
            hasLegacyData = true; // Assume there might be data, don't show backup dialog
          }

          // Only offer to restore from backup if this is truly a fresh install
          // (no state cache, no op-log, and no legacy data)
          if (!hasLegacyData) {
            await this._localBackupService.askForFileStoreBackupIfAvailable();
          }
        }
      }
      // trigger backup init after
      this._localBackupService.init();
    }
  }

  private async _checkIsSingleInstance(): Promise<boolean> {
    const channel = new BroadcastChannel('superProductivityTab');
    let isAnotherInstanceActive = false;
    let resolved = false;

    // 1. Listen for other instances saying "I'm here!"
    const checkListener = (msg: MessageEvent): void => {
      if (msg.data === 'alreadyOpenElsewhere') {
        isAnotherInstanceActive = true;
        resolved = true;
      }
    };
    channel.addEventListener('message', checkListener);

    // 2. Ask "Is anyone here?"
    channel.postMessage('newTabOpened');

    // 3. Wait for response with early exit - reduced from 150ms to 50ms
    // BroadcastChannel is synchronous within the same origin, so 50ms is sufficient
    await new Promise<void>((resolve) => {
      const checkInterval = setInterval(() => {
        if (resolved) {
          clearInterval(checkInterval);
          resolve();
        }
      }, 10);
      setTimeout(() => {
        clearInterval(checkInterval);
        resolve();
      }, 50);
    });

    channel.removeEventListener('message', checkListener);

    if (isAnotherInstanceActive) {
      return false;
    }

    // 4. If we are the only one, start listening for new tabs to warn them
    channel.addEventListener('message', (msg) => {
      if (msg.data === 'newTabOpened') {
        channel.postMessage('alreadyOpenElsewhere');
      }
    });

    return true;
  }

  private _showMultiInstanceBlocker(): void {
    const msg =
      'Super Productivity is already running in another tab. Please close this tab or the other one.';
    const style =
      'display: flex; align-items: center; justify-content: center; height: 100vh; text-align: center; font-family: sans-serif; padding: 2rem;';
    document.body.innerHTML = `
      <div style="${style}">
        <div>
          <h1>App is already open</h1>
          <p>${msg}</p>
        </div>
      </div>
    `;
  }

  private _isTourLikelyToBeShown(): boolean {
    if (localStorage.getItem(LS.IS_SKIP_TOUR)) {
      return false;
    }
    const ua = navigator.userAgent;
    if (ua === 'NIGHTWATCH' || ua.includes('PLAYWRIGHT')) {
      return false;
    }
    const projectList = this._projectService.list();
    return !projectList || projectList.length <= 2;
  }

  private _initOfflineBanner(): void {
    const needsInternet$ = combineLatest([
      this._store.select(selectSyncConfig),
      this._store.select(selectEnabledIssueProviders),
    ]).pipe(
      map(([syncConfig, enabledIssueProviders]) => {
        const hasCloudSync =
          syncConfig.syncProvider !== null &&
          syncConfig.syncProvider !== SyncProviderId.LocalFile;
        const hasIssueProviders = enabledIssueProviders.length > 0;
        return hasCloudSync || hasIssueProviders;
      }),
    );

    combineLatest([isOnline$, needsInternet$]).subscribe(([isOnline, needsInternet]) => {
      if (!isOnline && needsInternet) {
        this._bannerService.open({
          id: BannerId.Offline,
          ico: 'cloud_off',
          msg: T.APP.B_OFFLINE,
        });
      } else {
        this._bannerService.dismissAll(BannerId.Offline);
      }
    });
  }

  private _requestPersistence(): void {
    // A1 (#7925): always log the outcome so a #7892-style report carries the
    // durability state of the WebView store. Snack gating below is unchanged.
    const isNative = this._platformService.isNative;
    if (!navigator.storage) {
      Log.log('Persistence: navigator.storage unavailable', { isNative, IS_ELECTRON });
      return;
    }
    navigator.storage
      .persisted()
      .then((persisted) => {
        if (persisted) {
          Log.log('Persistence: already granted', { isNative, IS_ELECTRON });
          return;
        }
        return navigator.storage.persist().then((granted) => {
          Log.log('Persistence: persist() resolved', {
            granted,
            isNative,
            IS_ELECTRON,
          });
          // Native + Electron persistence is OS-managed (not subject to browser
          // eviction); also suppress during onboarding.
          if (
            !granted &&
            !isNative &&
            !IS_ELECTRON &&
            !OnboardingHintService.isOnboardingInProgress()
          ) {
            Log.warn('Persistence not allowed');
            this._snackService.open({ msg: T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED });
          }
        });
      })
      .catch((e) => {
        Log.log('Persistence: error', { isNative, IS_ELECTRON, error: e });
        const err = e && e.toString ? e.toString() : 'UNKNOWN';
        this._snackService.open({
          type: 'ERROR',
          msg: T.GLOBAL_SNACK.PERSISTENCE_ERROR,
          translateParams: { err },
        });
      });
  }

  private _checkAvailableStorage(): void {
    if (environment.production) {
      if ('storage' in navigator && 'estimate' in navigator.storage) {
        navigator.storage.estimate().then(({ usage, quota }) => {
          const u = usage || 0;
          const q = quota || 0;

          const percentUsed = Math.round((u / q) * 100);
          const usageInMib = Math.round(u / (1024 * 1024));
          const quotaInMib = Math.round(q / (1024 * 1024));
          const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;
          Log.log(details);
          if (quotaInMib - usageInMib <= 333) {
            alertDialog(
              `There is only very little disk space available (${
                quotaInMib - usageInMib
              }mb). This might affect how the app is running.`,
            );
          }
        });
      }
    }
  }

  private async _initPlugins(): Promise<void> {
    // Initialize plugin system
    try {
      // Wait for store hydration and sync to complete before initializing plugins.
      // Store hydration must finish so that _shouldAutoEnableMigrationPlugin
      // can query issueProviders from the store reliably.
      await Promise.all([
        this._dataInitStateService.isAllDataLoadedInitially$.pipe(take(1)).toPromise(),
        this._syncWrapperService.afterCurrentSyncDoneOrSyncDisabled$
          .pipe(take(1))
          .toPromise(),
      ]);
      await this._pluginService.initializePlugins();
      Log.log('Plugin system initialized after sync completed');
    } catch (error) {
      Log.err('Failed to initialize plugin system:', error);
    }
  }
}
