import {
  APP_INITIALIZER,
  enableProdMode,
  EnvironmentInjector,
  ErrorHandler,
  Injector,
  createEnvironmentInjector,
  importProvidersFrom,
  provideZonelessChangeDetection,
  SecurityContext,
} from '@angular/core';
import { registerLocaleData } from '@angular/common';

import { environment } from './environments/environment';
import { IS_ELECTRON } from './app/app.constants';
import {
  DEFAULT_LANGUAGE,
  DEFAULT_LOCALE_DATA,
  LocaleImportFns,
} from './app/core/locale.constants';
import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view';
import { androidInterface } from './app/features/android/android-interface';
import { AndroidBackButtonService } from './app/features/android/android-back-button.service';
import { IS_IOS_NATIVE, IS_NATIVE_PLATFORM } from './app/util/is-native-platform';
import { DataInitStateService } from './app/core/data-init/data-init-state.service';
// Type definitions for window.ea are in ./app/core/window-ea.d.ts
import { App as CapacitorApp } from '@capacitor/app';
import { GlobalErrorHandler } from './app/core/error-handler/global-error-handler.class';
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import {
  HTTP_INTERCEPTORS,
  provideHttpClient,
  withInterceptorsFromDi,
} from '@angular/common/http';
import { MarkdownModule, MARKED_OPTIONS, SANITIZE } from 'ngx-markdown';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import { IS_TOUCH_PRIMARY } from './app/util/is-mouse-primary';
import { FeatureStoresModule } from './app/root-store/feature-stores.module';
import {
  MATERIAL_ANIMATIONS,
  MatNativeDateModule,
  MAT_DATE_FORMATS,
  MatDateFormats,
  DateAdapter,
} from '@angular/material/core';
import { MatDatepickerIntl } from '@angular/material/datepicker';
import { FormlyConfigModule } from './app/ui/formly-config.module';
import { markedOptionsFactory } from './app/ui/marked-options-factory';
import { MaterialCssVarsModule } from 'angular-material-css-vars';
import { DEFAULT_TODAY_TAG_COLOR } from './app/features/work-context/work-context.const';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { ReminderModule } from './app/features/reminder/reminder.module';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import {
  PreloadAllModules,
  provideRouter,
  withHashLocation,
  withPreloading,
} from '@angular/router';
import { APP_ROUTES } from './app/app.routes';
import { StoreModule, Store } from '@ngrx/store';
import { META_REDUCERS } from './app/root-store/meta/meta-reducer-registry';
import { setOperationCaptureService } from './app/root-store/meta/task-shared-meta-reducers';
import { OperationCaptureService } from './app/op-log/capture/operation-capture.service';
import { EncryptionPasswordDialogOpenerService } from './app/imex/sync/encryption-password-dialog-opener.service';
import { DataInitService } from './app/core/data-init/data-init.service';
import { EffectsModule } from '@ngrx/effects';
// StoreDevtoolsModule lazy-loaded only in dev mode below
import { ReactiveFormsModule } from '@angular/forms';
import { ServiceWorkerModule } from '@angular/service-worker';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TRANSLATE_HTTP_LOADER_CONFIG } from '@ngx-translate/http-loader';
import { TranslateHttpLoaderWithFallback } from './app/core/http/translate-http-loader-with-fallback.class';
import { CdkDropListGroup } from '@angular/cdk/drag-drop';
import { AppComponent } from './app/app.component';
import { ShortTimeHtmlPipe } from './app/ui/pipes/short-time-html.pipe';
import { ShortTimePipe } from './app/ui/pipes/short-time.pipe';
import { BackgroundTask } from '@capawesome/capacitor-background-task';
import { PLUGIN_INITIALIZER_PROVIDER } from './app/plugins/plugin-initializer';
import { initializeMatMenuTouchFix } from './app/features/tasks/task-context-menu/mat-menu-touch-monkey-patch';
import { Log, SyncLog } from './app/core/log';
import { setLegacyKdfWarningHandler } from '@sp/sync-core';
import { OperationWriteFlushService } from './app/op-log/sync/operation-write-flush.service';
import { TaskService } from './app/features/tasks/task.service';
import { PluginOAuthRedirectHandler } from './app/plugins/oauth/plugin-oauth-redirect.handler';
import { OAuthCallbackHandlerService } from './app/imex/sync/oauth-callback-handler.service';
import { GlobalConfigService } from './app/features/config/global-config.service';
import { LocaleDatePipe } from './app/ui/pipes/locale-date.pipe';
import { DateTimeFormatService } from './app/core/date-time-format/date-time-format.service';
import { CustomDateAdapter } from './app/core/date-time-format/custom-date-adapter';
import { TranslateMatDatepickerIntl } from './app/core/date-time-format/translate-mat-datepicker-intl';
import { suspendAudioContext, unlockAudioContext } from './app/util/audio-context';
import { NetworkRetryInterceptorService } from './app/core/http/network-retry-interceptor.service';

if (environment.production || environment.stage) {
  enableProdMode();
}

// Window.ea declaration is in src/app/core/window-ea.d.ts

// Module-level injector for use in Capacitor lifecycle handlers.
// Set after Angular bootstrap completes.
let appInjector: Injector | null = null;

// Register one-time user gesture listener to unlock AudioContext.
// Required on iOS/Android where AudioContext starts suspended.
unlockAudioContext();

// Surface a deprecation warning the first time legacy PBKDF2 ciphertext is
// decrypted in this session. The encryption layer invokes this handler on
// every legacy decrypt; we throttle to one log per session.
let _hasWarnedLegacyKdf = false;
setLegacyKdfWarningHandler(() => {
  if (_hasWarnedLegacyKdf) return;
  _hasWarnedLegacyKdf = true;
  SyncLog.log(
    '[DEPRECATION] Legacy PBKDF2 encryption detected. Consider re-syncing to migrate to Argon2id.',
  );
});

bootstrapApplication(AppComponent, {
  providers: [
    // Provide configuration for TranslateHttpLoader
    {
      provide: TRANSLATE_HTTP_LOADER_CONFIG,
      useValue: {
        prefix: './assets/i18n/',
        suffix: '.json',
      },
    },
    importProvidersFrom(
      FeatureStoresModule,
      MatNativeDateModule,
      FormlyConfigModule,
      MarkdownModule.forRoot({
        markedOptions: {
          provide: MARKED_OPTIONS,
          useFactory: markedOptionsFactory,
        },
        sanitize: { provide: SANITIZE, useValue: SecurityContext.HTML },
      }),
      MaterialCssVarsModule.forRoot({
        primary: DEFAULT_TODAY_TAG_COLOR,
      }),
      MatSidenavModule,
      MatBottomSheetModule,
      ReminderModule,
      // External
      BrowserModule,
      // NOTE: both need to be present to use forFeature stores
      // Meta-reducers are defined in meta-reducer-registry.ts with detailed phase documentation
      StoreModule.forRoot(undefined, {
        metaReducers: META_REDUCERS,
        ...(environment.production
          ? {
              runtimeChecks: {
                strictStateImmutability: false,
                strictActionImmutability: false,
                strictStateSerializability: false,
                strictActionSerializability: false,
              },
            }
          : {
              runtimeChecks: {
                strictStateImmutability: true,
                strictActionImmutability: true,
                strictStateSerializability: true,
                strictActionSerializability: true,
                strictActionTypeUniqueness: true,
              },
            }),
      }),
      EffectsModule.forRoot([]),
      // StoreDevtoolsModule lazy-loaded in dev mode after bootstrap
      ReactiveFormsModule,
      ServiceWorkerModule.register('ngsw-worker.js', {
        enabled:
          !IS_ELECTRON &&
          !IS_NATIVE_PLATFORM &&
          (environment.production || environment.stage),
        // Register the ServiceWorker as soon as the application is stable
        // or after 30 seconds (whichever comes first).
        registrationStrategy: 'registerWhenStable:30000',
      }),
      TranslateModule.forRoot({
        fallbackLang: DEFAULT_LANGUAGE,
        loader: {
          provide: TranslateLoader,
          useClass: TranslateHttpLoaderWithFallback,
        },
      }),
      CdkDropListGroup,
    ),
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
    provideHttpClient(withInterceptorsFromDi()),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: NetworkRetryInterceptorService,
      multi: true,
    },
    LocaleDatePipe,
    ShortTimeHtmlPipe,
    ShortTimePipe,
    { provide: DateAdapter, useClass: CustomDateAdapter },
    { provide: MatDatepickerIntl, useClass: TranslateMatDatepickerIntl },
    {
      provide: MAT_DATE_FORMATS,
      useFactory: (dateTimeFormatService: DateTimeFormatService): MatDateFormats => {
        // Use getters so dateInput re-evaluates when the user changes locale
        return {
          parse: {
            get dateInput(): string {
              return dateTimeFormatService.dateFormat().raw;
            },
            timeInput: { hour: 'numeric', minute: 'numeric' },
          },
          display: {
            get dateInput(): string {
              return dateTimeFormatService.dateFormat().raw;
            },
            monthYearLabel: { year: 'numeric', month: 'long' },
            dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
            monthYearA11yLabel: { year: 'numeric', month: 'long' },
            timeInput: { hour: 'numeric', minute: 'numeric' },
            timeOptionLabel: { hour: 'numeric', minute: 'numeric' },
          },
        };
      },
      deps: [DateTimeFormatService],
    },
    {
      provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
      useValue: { appearance: 'fill', subscriptSizing: 'dynamic' },
    },
    // Disable autofocus for touch-primary devices to prevent virtual keyboard popup
    ...(IS_TOUCH_PRIMARY
      ? [{ provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { autoFocus: false } }]
      : []),
    provideAnimationsAsync(),
    {
      provide: MATERIAL_ANIMATIONS,
      deps: [GlobalConfigService],
      useFactory: (globalConfigService: GlobalConfigService) => ({
        get animationsDisabled(): boolean {
          return globalConfigService.misc()?.isDisableAnimations ?? false;
        },
      }),
    },
    provideRouter(APP_ROUTES, withHashLocation(), withPreloading(PreloadAllModules)),
    PLUGIN_INITIALIZER_PROVIDER,
    provideZonelessChangeDetection(),
    // Initialize operation capture service for synchronous state change capture
    // This must run before any persistent actions are dispatched
    {
      provide: APP_INITIALIZER,
      useFactory: (captureService: OperationCaptureService) => {
        return () => {
          setOperationCaptureService(captureService);
        };
      },
      deps: [OperationCaptureService],
      multi: true,
    },
    // Ensure DataInitService is instantiated at bootstrap.
    // Its constructor triggers reInit() -> hydrateStore() -> loadAllData into NgRx.
    {
      provide: APP_INITIALIZER,
      useFactory: (_dataInit: DataInitService) => {
        return () => {};
      },
      deps: [DataInitService],
      multi: true,
    },
    // Initialize encryption password dialog opener for static form config functions
    {
      provide: APP_INITIALIZER,
      useFactory: (_opener: EncryptionPasswordDialogOpenerService) => {
        // Service constructor self-registers the module-level reference
        return () => {};
      },
      deps: [EncryptionPasswordDialogOpenerService],
      multi: true,
    },
    // Ensure PluginOAuthRedirectHandler is instantiated at bootstrap.
    // Its constructor registers platform-specific listeners (postMessage / Electron IPC)
    // that bridge OAuth redirect callbacks to PluginOAuthService.
    {
      provide: APP_INITIALIZER,
      useFactory: (_handler: PluginOAuthRedirectHandler) => {
        return () => {};
      },
      deps: [PluginOAuthRedirectHandler],
      multi: true,
    },
    // Ensure OAuthCallbackHandlerService is instantiated at bootstrap on native platforms.
    // Its constructor registers Capacitor's appUrlOpen listener that bridges
    // both Dropbox and plugin OAuth redirect callbacks.
    {
      provide: APP_INITIALIZER,
      useFactory: (_handler: OAuthCallbackHandlerService) => {
        return () => {};
      },
      deps: [OAuthCallbackHandlerService],
      multi: true,
    },
    // Note: ImmediateUploadService now initializes itself in constructor
    // after DataInitStateService.isAllDataLoadedInitially$ fires to avoid
    // race condition where upload attempts happen before sync config is loaded
  ],
}).then((appRef) => {
  appInjector = appRef.injector;

  // Expose store + HydrationStateService for e2e tests in dev/stage builds.
  // Used by the screenshot pipeline to flip locale / customTheme inside a
  // single session (see e2e/store-screenshots/helpers.ts) and by #6230
  // recurring-task tests. Stripped from production via the env guard.
  if (!environment.production && !environment.stage) {
    const storeRef = appRef.injector.get(Store);
    import('./app/op-log/apply/hydration-state.service').then((m) => {
      (window as unknown as { __e2eTestHelpers?: unknown }).__e2eTestHelpers = {
        store: storeRef,
        hydrationState: appRef.injector.get(m.HydrationStateService),
      };
    });
  }

  // Dismiss native startup overlay after all data is loaded (Android only)
  if (IS_ANDROID_WEB_VIEW) {
    appRef.injector.get(DataInitStateService).isAllDataLoadedInitially$.subscribe(() => {
      import('./app/core/startup-overlay/startup-overlay.service').then((m) => {
        appRef.injector.get(m.StartupOverlayService).processAndDismiss();
      });
    });
  }

  // Initialize touch fix for Material menus
  initializeMatMenuTouchFix();

  // Register default locale immediately (statically imported, no network fetch)
  registerLocaleData(DEFAULT_LOCALE_DATA, DEFAULT_LANGUAGE);

  // Lazily load and register remaining locales during idle time
  const registerRemainingLocales = (): void => {
    Object.keys(LocaleImportFns).forEach((locale) => {
      if (locale !== DEFAULT_LANGUAGE) {
        LocaleImportFns[locale as keyof typeof LocaleImportFns]()
          .then((m) => {
            registerLocaleData(m.default, locale);
          })
          .catch((e) => Log.err(`Failed to load locale ${locale}`, e));
      }
    });
  };

  // Lazily load and register focus-mode effects during idle time.
  // Safe to defer: focus-mode requires explicit user activation (clicking the
  // focus button), which cannot happen before idle callback fires.
  const registerLazyEffects = async (): Promise<void> => {
    const { FocusModeEffects } =
      await import('./app/features/focus-mode/store/focus-mode.effects');
    const envInjector = appRef.injector.get(EnvironmentInjector);
    createEnvironmentInjector(
      [importProvidersFrom(EffectsModule.forFeature([FocusModeEffects]))],
      envInjector,
    );
  };

  // Lazily load store devtools only in dev mode
  const registerStoreDevtools = async (): Promise<void> => {
    if (environment.production || environment.stage) {
      return;
    }
    const { StoreDevtoolsModule } = await import('@ngrx/store-devtools');
    const envInjector = appRef.injector.get(EnvironmentInjector);
    createEnvironmentInjector(
      [
        importProvidersFrom(
          StoreDevtoolsModule.instrument({
            maxAge: 15,
            logOnly: false,
            actionsBlocklist: ['[TimeTracking] Add time spent'],
          }),
        ),
      ],
      envInjector,
    );
  };

  if (typeof requestIdleCallback === 'function') {
    requestIdleCallback(() => registerRemainingLocales());
    requestIdleCallback(() =>
      registerLazyEffects().catch((e) => Log.err('Failed to register lazy effects', e)),
    );
    requestIdleCallback(() =>
      registerStoreDevtools().catch((e) => Log.err('Failed to register devtools', e)),
    );
  } else {
    setTimeout(() => registerRemainingLocales(), 0);
    setTimeout(
      () =>
        registerLazyEffects().catch((e) => Log.err('Failed to register lazy effects', e)),
      0,
    );
    setTimeout(
      () =>
        registerStoreDevtools().catch((e) => Log.err('Failed to register devtools', e)),
      0,
    );
  }

  // TODO make asset caching work for electron

  if (
    'serviceWorker' in navigator &&
    (environment.production || environment.stage) &&
    !IS_ELECTRON &&
    !IS_NATIVE_PLATFORM
  ) {
    Log.log('Registering Service worker');
    return navigator.serviceWorker.register('ngsw-worker.js').catch((err: unknown) => {
      Log.log('Service Worker Registration Error');
      Log.err(err);
    });
  } else if ('serviceWorker' in navigator && (IS_ELECTRON || IS_NATIVE_PLATFORM)) {
    navigator.serviceWorker
      .getRegistrations()
      .then((registrations) => {
        for (const registration of registrations) {
          registration.unregister();
        }
      })
      .catch((e) => {
        Log.err('ERROR when unregistering service worker');
        Log.err(e);
      });
  }
  return undefined;
});

// fix mobile scrolling while dragging
window.addEventListener('touchmove', () => {});

if (!(environment.production || environment.stage) && IS_ANDROID_WEB_VIEW) {
  setTimeout(() => {
    androidInterface.showToast('Android DEV works');
    Log.log(androidInterface);
  }, 1000);
}

// CAPACITOR STUFF
// ---------------

// Android-specific: Handle back button
if (IS_ANDROID_WEB_VIEW) {
  CapacitorApp.addListener('backButton', ({ canGoBack }) => {
    // Delegate to the Angular service so back from a top-level destination pops
    // to the start destination / exits per Android guidelines (issue #7972).
    const backButtonService = appInjector?.get(AndroidBackButtonService);
    if (backButtonService) {
      backButtonService.handleBackButton(canGoBack);
    } else if (!canGoBack) {
      // Pre-bootstrap fallback (back pressed before Angular is ready).
      CapacitorApp.minimizeApp();
    } else {
      window.history.back();
    }
  });
}

// Flush pending operations from in-memory FIFO queue to IndexedDB.
// Called when the app goes to background to prevent data loss if the OS kills the app.
const flushPendingOperations = async (platform: string): Promise<void> => {
  if (!appInjector) {
    Log.log(`${platform} background: app not yet bootstrapped, skipping flush`);
    return;
  }
  const flushService = appInjector.get(OperationWriteFlushService);
  await flushService.flushPendingWrites();
  Log.log(`${platform} background: operation flush complete`);
};

// Android: Flush pending operations to IndexedDB when app goes to background.
// Without this, operations in the in-memory FIFO queue are lost if the OS kills the app.
if (IS_ANDROID_WEB_VIEW) {
  CapacitorApp.addListener('appStateChange', async ({ isActive }) => {
    if (isActive) {
      return;
    }
    // Release the audio output stream so a silent-but-running AudioContext does
    // not keep the audio hardware (and the process) awake in the background (#8243).
    suspendAudioContext();
    const taskId = await BackgroundTask.beforeExit(async () => {
      try {
        await flushPendingOperations('Android');
      } catch (e) {
        Log.err('Android background: operation flush failed', e);
      }
      BackgroundTask.finish({ taskId });
    });
  });
}

// iOS: Flush pending operations to IndexedDB when app goes to background.
if (IS_IOS_NATIVE) {
  CapacitorApp.addListener('appStateChange', async ({ isActive }) => {
    if (isActive) {
      return;
    }
    // Release the audio output stream so a silent-but-running AudioContext does
    // not keep the audio hardware (and the process) awake in the background (#8243).
    suspendAudioContext();
    const taskId = await BackgroundTask.beforeExit(async () => {
      try {
        // Dispatch any accumulated tracked time so it is enqueued before the
        // op-log drain below. iOS suspends the WebView seconds after this, so
        // both the dispatch and the persist must happen inside this budget.
        appInjector?.get(TaskService).flushAccumulatedTimeSpent();
        await flushPendingOperations('iOS');
      } catch (e) {
        Log.err('iOS background: operation flush failed', e);
      }
      BackgroundTask.finish({ taskId });
    });
  });

  // Handle app URL open (for OAuth callbacks, deep links, etc.)
  CapacitorApp.addListener('appUrlOpen', (event) => {
    Log.log('iOS app URL open', event.url);
    // Handle OAuth callbacks or deep links here
    // The URL will be passed to the app when opened via custom scheme
  });
}
