import { inject, Injectable } from '@angular/core';
import { SyncProviderManager } from '../../op-log/sync-providers/provider-manager.service';
import { GlobalConfigService } from '../../features/config/global-config.service';
import { combineLatest, from, Observable, of } from 'rxjs';
import { SyncConfig } from '../../features/config/global-config.model';
import { shareReplay, switchMap, tap } from 'rxjs/operators';
import {
  CurrentProviderPrivateCfg,
  PrivateCfgByProviderId,
  SyncProviderId,
} from '../../op-log/sync-exports';
import { DEFAULT_GLOBAL_CONFIG } from '../../features/config/default-global-config.const';
import { SyncLog } from '../../core/log';
import { clearSessionKeyCache } from '@sp/sync-core';
import type { SuperSyncPrivateCfg } from '@sp/sync-providers/super-sync';
import { SyncWrapperService } from './sync-wrapper.service';
import { HAS_OFFICIAL_ONEDRIVE_CLIENT_ID } from './onedrive-auth-mode.const';

// Maps sync providers to their corresponding form field in SyncConfig
// Dropbox is null because it doesn't store settings in the form (uses OAuth)
const PROP_MAP_TO_FORM: Record<SyncProviderId, keyof SyncConfig | null> = {
  [SyncProviderId.LocalFile]: 'localFileSync',
  [SyncProviderId.WebDAV]: 'webDav',
  [SyncProviderId.SuperSync]: 'superSync',
  [SyncProviderId.Nextcloud]: 'nextcloud',
  [SyncProviderId.OneDrive]: 'oneDrive',
  [SyncProviderId.Dropbox]: null,
};

// Ensures all required fields have empty string defaults to prevent undefined/null errors
// when providers expect string values (e.g., WebDAV API calls fail with undefined URLs)
// Fields that should never be logged, even in development
const SENSITIVE_FIELDS = [
  'password',
  'encryptKey',
  'accessToken',
  'refreshToken',
  'loginName',
  'userName',
];

/**
 * Redacts sensitive fields from an object for safe logging.
 * Replaces sensitive values with '[REDACTED]' to prevent credential exposure.
 */
const redactSensitiveFields = (obj: unknown): unknown => {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(redactSensitiveFields);
  }

  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
    if (SENSITIVE_FIELDS.includes(key)) {
      result[key] = value ? '[REDACTED]' : '';
    } else if (typeof value === 'object' && value !== null) {
      result[key] = redactSensitiveFields(value);
    } else {
      result[key] = value;
    }
  }
  return result;
};

const PROVIDER_FIELD_DEFAULTS: Record<
  SyncProviderId,
  Record<string, string | boolean | number>
> = {
  [SyncProviderId.WebDAV]: {
    baseUrl: '',
    userName: '',
    password: '',
    syncFolderPath: '',
    encryptKey: '',
  },
  [SyncProviderId.SuperSync]: {
    baseUrl: '',
    userName: '',
    password: '',
    accessToken: '',
    syncFolderPath: '',
    encryptKey: '',
    isEncryptionEnabled: false,
  },
  [SyncProviderId.Nextcloud]: {
    serverUrl: '',
    loginName: '',
    userName: '',
    password: '',
    syncFolderPath: '',
    encryptKey: '',
  },
  [SyncProviderId.LocalFile]: {
    // syncFolderPath is intentionally omitted: post-#8228 the sync folder
    // path is owned main-side (electron/local-file-sync.ts) so a compromised
    // renderer cannot rewrite it via the credential store.
    encryptKey: '',
  },
  [SyncProviderId.Dropbox]: {
    encryptKey: '',
  },
  [SyncProviderId.OneDrive]: {
    useCustomApp: !HAS_OFFICIAL_ONEDRIVE_CLIENT_ID,
    clientId: '',
    tenantId: 'common',
    syncFolderPath: 'Super Productivity',
    accessToken: '',
    refreshToken: '',
    tokenExpiresAt: 0,
    encryptKey: '',
  },
};

@Injectable({
  providedIn: 'root',
})
export class SyncConfigService {
  private _providerManager = inject(SyncProviderManager);
  private _globalConfigService = inject(GlobalConfigService);
  private _syncWrapper = inject(SyncWrapperService);

  private _lastSettings: SyncConfig | null = null;

  private _deriveEncryptionState(
    baseConfig: SyncConfig,
    currentProviderCfg: CurrentProviderPrivateCfg | null,
  ): { isEncryptionEnabled: boolean; encryptKey: string } {
    if (!currentProviderCfg) {
      return {
        isEncryptionEnabled: baseConfig.isEncryptionEnabled ?? false,
        encryptKey: '',
      };
    }

    const privateCfg = currentProviderCfg.privateCfg as {
      encryptKey?: string;
      isEncryptionEnabled?: boolean;
    } | null;
    if (!privateCfg) {
      return {
        isEncryptionEnabled: baseConfig.isEncryptionEnabled ?? false,
        encryptKey: '',
      };
    }

    const encryptKey = privateCfg.encryptKey ?? '';

    if (currentProviderCfg.providerId === SyncProviderId.SuperSync) {
      return {
        isEncryptionEnabled: privateCfg.isEncryptionEnabled ?? false,
        encryptKey,
      };
    }

    // File-based providers: prefer the durable per-provider intent flag over key
    // presence (GHSA-9544-hjjr-fg8h). A silently dropped key must still show the
    // form as "encryption on" so the user isn't misled into thinking they turned
    // it off, and so a settings save preserves the intent instead of flipping it
    // off. Pre-fix configs without the flag fall back to key presence.
    return {
      isEncryptionEnabled: privateCfg.isEncryptionEnabled ?? !!encryptKey,
      encryptKey,
    };
  }

  readonly syncSettingsForm$: Observable<SyncConfig> = combineLatest([
    this._globalConfigService.sync$,
    this._providerManager.currentProviderPrivateCfg$,
  ]).pipe(
    switchMap(([syncCfg, currentProviderCfg]) => {
      // Base config with defaults
      // Deep merge provider-specific configs to preserve defaults like superSync.baseUrl
      // Without this, a stored config with superSync: {} would lose the default baseUrl
      const baseConfig = {
        ...DEFAULT_GLOBAL_CONFIG.sync,
        ...syncCfg,
        superSync: {
          ...DEFAULT_GLOBAL_CONFIG.sync.superSync,
          ...syncCfg?.superSync,
        },
        webDav: {
          ...DEFAULT_GLOBAL_CONFIG.sync.webDav,
          ...syncCfg?.webDav,
        },
        localFileSync: {
          ...DEFAULT_GLOBAL_CONFIG.sync.localFileSync,
          ...syncCfg?.localFileSync,
        },
        nextcloud: {
          ...DEFAULT_GLOBAL_CONFIG.sync.nextcloud,
          ...syncCfg?.nextcloud,
        },
        oneDrive: {
          ...DEFAULT_GLOBAL_CONFIG.sync.oneDrive,
          ...syncCfg?.oneDrive,
        },
      };

      // If no provider is active, return base config with empty encryption key
      if (!currentProviderCfg) {
        return from(
          fetch('/assets/sync-config-default-override.json')
            .then((res) => {
              if (!res.ok) {
                throw new Error(`HTTP ${res.status}`);
              }
              return res.json();
            })
            .then((defaultOverride) => {
              return {
                ...baseConfig,
                ...defaultOverride,
                webDav: {
                  ...baseConfig.webDav,
                  ...defaultOverride.webDav,
                },
                encryptKey: '',
              };
            })
            .catch((e) => {
              SyncLog.warn(
                'Failed to load sync-config-default-override.json, using base config:',
                e,
              );
              return {
                ...baseConfig,
                encryptKey: '',
              };
            }),
        );
      }

      const prop = PROP_MAP_TO_FORM[currentProviderCfg.providerId];

      const { isEncryptionEnabled, encryptKey } = this._deriveEncryptionState(
        baseConfig,
        currentProviderCfg,
      );

      // Create config with provider-specific settings
      const result: SyncConfig = {
        ...baseConfig,
        encryptKey,
        isEncryptionEnabled,
        // Reset provider-specific configs to defaults first
        localFileSync: DEFAULT_GLOBAL_CONFIG.sync.localFileSync,
        webDav: DEFAULT_GLOBAL_CONFIG.sync.webDav,
        superSync: DEFAULT_GLOBAL_CONFIG.sync.superSync,
        nextcloud: DEFAULT_GLOBAL_CONFIG.sync.nextcloud,
        oneDrive: DEFAULT_GLOBAL_CONFIG.sync.oneDrive,
      };

      // Add current provider config if applicable
      if (prop && currentProviderCfg.privateCfg) {
        // TypeScript limitation: dynamic key assignment on union types requires cast
        (result as Record<string, unknown>)[prop] = currentProviderCfg.privateCfg;
      }

      return of(result);
    }),
    // Keep _lastSettings in sync so Formly modelChange doesn't trigger redundant saves
    tap((v) => {
      this._lastSettings = v;
      SyncLog.log('syncSettingsForm$', redactSensitiveFields(v));
    }),
    // Cache the latest emission across all subscribers (refCount:false) so a
    // dialog opened later from the header — when the settings page is not
    // mounted — can replay without re-running combineLatest and re-fetching
    // /assets/sync-config-default-override.json.
    shareReplay({ bufferSize: 1, refCount: false }),
  );

  async updateEncryptionPassword(
    pwd: string,
    syncProviderId?: SyncProviderId,
  ): Promise<void> {
    const activeProvider = syncProviderId
      ? await this._providerManager.getProviderById(syncProviderId)
      : this._providerManager.getActiveProvider();
    if (!activeProvider) {
      // During initial sync setup, no provider exists yet to store the key.
      // The key will be saved when the user completes provider configuration.
      SyncLog.err(
        'No active sync provider found when trying to update encryption password',
      );
      return;
    }
    const oldConfig = await activeProvider.privateCfg.load();

    // Build new config - for SuperSync, always enable encryption when password is set
    const newConfig = {
      ...oldConfig,
      encryptKey: pwd,
    } as PrivateCfgByProviderId<SyncProviderId>;

    // For SuperSync, explicitly enable encryption
    if (activeProvider.id === SyncProviderId.SuperSync) {
      (newConfig as SuperSyncPrivateCfg).isEncryptionEnabled = true;
    }

    await this._providerManager.setProviderConfig(activeProvider.id, newConfig);

    // Clear cached encryption keys to force re-derivation with new password
    clearSessionKeyCache();
    // Allow encryption dialogs to appear again after password change
    this._syncWrapper.clearEncryptionDialogSuppression();
  }

  async updateSettingsFromForm(newSettings: SyncConfig, isForce = false): Promise<void> {
    // Formly can trigger multiple updates for a single user action, causing sync conflicts
    // and unnecessary API calls. This check prevents duplicate saves.
    const isEqual = JSON.stringify(this._lastSettings) === JSON.stringify(newSettings);
    if (isEqual && !isForce) {
      return;
    }
    this._lastSettings = newSettings;

    const providerId = newSettings.syncProvider as SyncProviderId | null;
    type SyncPublicConfig = Omit<
      SyncConfig,
      'encryptKey' | 'webDav' | 'localFileSync' | 'superSync' | 'nextcloud' | 'oneDrive'
    >;

    // Split settings into public (global config) and private (credentials/secrets)
    // to maintain security boundaries - credentials never go to global config
    const superSync = newSettings.superSync;
    // Only include optional booleans when explicitly set, so partial form
    // updates don't silently overwrite prior true values with undefined.
    let globalConfig: SyncPublicConfig = {
      isEnabled: newSettings.isEnabled ?? false,
      syncProvider: newSettings.syncProvider ?? null,
      syncInterval: newSettings.syncInterval ?? 300000,
      ...(newSettings.isEncryptionEnabled !== undefined
        ? { isEncryptionEnabled: newSettings.isEncryptionEnabled }
        : {}),
      ...(newSettings.isCompressionEnabled !== undefined
        ? { isCompressionEnabled: newSettings.isCompressionEnabled }
        : {}),
      ...(newSettings.isManualSyncOnly !== undefined
        ? { isManualSyncOnly: newSettings.isManualSyncOnly }
        : {}),
    };
    // Provider-specific settings (URLs, credentials) must be stored securely
    if (providerId) {
      await this._updatePrivateConfig(providerId, newSettings);
    }

    // For SuperSync, propagate provider-specific encryption setting to global config
    // This ensures sync services see isEncryptionEnabled=true when SuperSync encryption is enabled
    // Note: We need to check the SAVED private config because Formly doesn't include hidden fields
    if (providerId === SyncProviderId.SuperSync) {
      const activeProvider = await this._providerManager.getProviderById(providerId);
      const savedPrivateCfg = activeProvider
        ? await activeProvider.privateCfg.load()
        : null;
      const isEncryptionEnabled =
        superSync?.isEncryptionEnabled ??
        (savedPrivateCfg as { isEncryptionEnabled?: boolean } | null)
          ?.isEncryptionEnabled ??
        false;
      globalConfig = {
        ...globalConfig,
        isEncryptionEnabled,
      };
    }

    this._globalConfigService.updateSection('sync', globalConfig as SyncConfig);
  }

  private async _updatePrivateConfig(
    providerId: SyncProviderId,
    settings: SyncConfig,
  ): Promise<void> {
    const prop = PROP_MAP_TO_FORM[providerId];

    // Load existing config to preserve OAuth tokens and other settings
    const activeProvider = await this._providerManager.getProviderById(providerId);
    const oldConfig = activeProvider ? await activeProvider.privateCfg.load() : {};

    // Form fields contain provider-specific settings, but Dropbox uses OAuth tokens
    // stored elsewhere, so it only needs the encryption key
    const privateConfigProviderSpecific = prop ? settings[prop] || {} : {};

    // Start with defaults to ensure API calls won't fail due to undefined values,
    // then overlay old config to preserve existing data (like OAuth tokens),
    // then overlay user settings, and always include encryption key for data security
    // NOTE: that we need the old config here in order not to overwrite other private stuff like tokens
    const providerCfgAsRecord = privateConfigProviderSpecific as Record<string, unknown>;

    // Filter out empty/undefined values from form to preserve existing credentials
    // This prevents resetOnHide and other form behaviors from clearing saved tokens
    // Empty credentials should be cleared by disabling the provider, not by form state
    const nonEmptyFormValues = Object.entries(providerCfgAsRecord).reduce(
      (acc, [key, value]) => {
        if (providerId === SyncProviderId.Nextcloud && key === 'loginName') {
          if (value !== undefined && value !== null) {
            acc[key] = typeof value === 'string' ? value : '';
          }
          return acc;
        }
        // Only include values that are truthy OR explicitly false/0
        // Skip: undefined, null, empty string
        if (value !== undefined && value !== null && value !== '') {
          acc[key] = value;
        }
        return acc;
      },
      {} as Record<string, unknown>,
    );

    // The provider's saved config is the source of truth after SuperSyncEncryptionToggleService runs
    // oldConfig is loaded from activeProvider.privateCfg.load()
    // When encryption is explicitly disabled, we must clear encryptKey regardless of form state
    const isEncryptionDisabledInSavedConfig =
      providerId === SyncProviderId.SuperSync &&
      (oldConfig as { isEncryptionEnabled?: boolean })?.isEncryptionEnabled === false;

    const savedEncryptKey = (oldConfig as { encryptKey?: string })?.encryptKey ?? '';

    // Resolve encryptKey based on provider type:
    // - SuperSync: encryptKey is managed via dedicated dialogs (EnableEncryption, ChangePassword,
    //   HandleDecryptError), NOT via the form. Always preserve savedEncryptKey unless disabled.
    // - File-based providers (WebDAV, LocalFile, Dropbox): encryptKey can be set via form,
    //   so use settings.encryptKey as fallback for new configs.
    let resolvedEncryptKey: string;
    if (isEncryptionDisabledInSavedConfig) {
      resolvedEncryptKey = '';
    } else if (providerId === SyncProviderId.SuperSync) {
      // For SuperSync, only use form if explicitly provided, otherwise preserve saved
      resolvedEncryptKey = (nonEmptyFormValues?.encryptKey as string) || savedEncryptKey;
    } else {
      // For file-based providers, use form values with settings.encryptKey as fallback
      resolvedEncryptKey =
        (nonEmptyFormValues?.encryptKey as string) || settings.encryptKey || '';
    }

    // For SuperSync, isEncryptionEnabled is managed exclusively by dedicated dialogs
    // (EnableEncryption, DisableEncryption, HandleDecryptError), NOT the form.
    // Preserve the saved value to prevent accidental overwrites during config saves.
    //
    // For file-based providers, persist the durable per-provider intent flag
    // (GHSA-9544-hjjr-fg8h): PRESERVE an existing value so a routine save while
    // the key is missing cannot silently disarm the plaintext-upload guard, and
    // otherwise backfill from the key present at save time (capturing intent
    // while the key still proves it, before any later silent drop).
    const oldIsEncryptionEnabled = (oldConfig as { isEncryptionEnabled?: boolean })
      ?.isEncryptionEnabled;
    const savedIsEncryptionEnabled =
      providerId === SyncProviderId.SuperSync
        ? oldIsEncryptionEnabled
        : (oldIsEncryptionEnabled ?? !!resolvedEncryptKey);

    const configWithDefaults = {
      ...PROVIDER_FIELD_DEFAULTS[providerId],
      ...oldConfig,
      ...nonEmptyFormValues, // Only non-empty values overwrite saved config
      encryptKey: resolvedEncryptKey,
      ...(savedIsEncryptionEnabled !== undefined
        ? { isEncryptionEnabled: savedIsEncryptionEnabled }
        : {}),
    };

    // Check if encryption settings changed to clear cached keys
    const oldEncryptKey = (oldConfig as { encryptKey?: string })?.encryptKey;
    const newEncryptKey = configWithDefaults.encryptKey as string;
    const isEncryptionChanged = oldEncryptKey !== newEncryptKey;

    await this._providerManager.setProviderConfig(
      providerId,
      configWithDefaults as PrivateCfgByProviderId<SyncProviderId>,
    );

    // Clear cache on ANY encryption change (not just disable)
    if (isEncryptionChanged && (oldEncryptKey || newEncryptKey)) {
      SyncLog.normal(
        'SyncConfigService: Encryption settings changed, clearing cached keys',
      );
      clearSessionKeyCache();
    }
  }
}
