import type { BaseLogger, BaseProvider, PreparedEffect, SecretValue } from '@kubricate/core';

import type { KubricateConfig } from '../../types.js';
import { SecretManagerEngine, type MergedSecretManager } from './SecretManagerEngine.js';
import type { ConfigConflictOptions, ConflictLevel, ConflictStrategy, SecretsOrchestratorOptions } from './types.js';

interface ResolvedSecret {
  key: string;
  value: SecretValue;
  providerName: string;
  managerName: string;
}

type PreparedEffectWithMeta = PreparedEffect & {
  providerName: string;
  managerName: string;
  secretType: string;
  identifier: string | undefined; // optional, depends on provider
};

/**
 * SecretsOrchestrator
 *
 * @description
 * Central orchestration engine responsible for:
 * - Validating secret configuration and managers
 * - Loading and resolving all declared secrets
 * - Preparing provider-specific effects
 * - Applying conflict resolution strategies (intraProvider, crossProvider, crossManager)
 * - Producing a fully merged, finalized list of secret effects ready for output (e.g., YAML, JSON, etc.)
 *
 * @remarks
 * - Acts as the internal core behind `kubricate secret apply`.
 * - Ensures predictable, auditable, and conflict-safe secret generation.
 * - Delegates provider-specific behavior to registered providers (e.g., mergeSecrets, prepare).
 *
 * @usage
 * Typically called via:
 *
 * ```ts
 * const orchestrator = SecretsOrchestrator.create(options);
 * const effects = await orchestrator.apply();
 * ```
 *
 * @throws {Error}
 * If configuration, validation, or merging fails at any stage.
 */
export class SecretsOrchestrator {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private providerCache = new Map<string, any>();

  constructor(
    private engine: SecretManagerEngine,
    private logger: BaseLogger
  ) {}

  /**
   * Factory method to create a SecretsOrchestrator instance from options.
   */
  static create(options: SecretsOrchestratorOptions): SecretsOrchestrator {
    const engine = new SecretManagerEngine(options);
    return new SecretsOrchestrator(engine, options.logger);
  }

  /**
   * Validates the project configuration and all registered secret managers.
   *
   * @remarks
   * This is automatically called by commands like `kubricate secret apply` and `kubricate secret validate`.
   *
   * @description
   * Performs full validation across:
   * - Configuration schema (e.g., strictConflictMode rules, secret manager presence)
   * - SecretManager instances and their attached connectors
   * - Ensures all declared secrets can be loaded without error
   *
   * Logs important validation steps for traceability.
   *
   * @returns {Promise<MergedSecretManager>} A fully validated set of collected secret managers.
   *
   * @throws {Error}
   * - If a configuration violation is detected (e.g., invalid strictConflictMode usage).
   * - If a SecretManager or connector fails to validate or load secrets.
   */
  async validate(): Promise<MergedSecretManager> {
    // 1. Validate config options (e.g., strictConflictMode)
    this.validateConfig(this.engine.options.config);

    // 2. Validate secret managers and connectors
    const managers = this.engine.collect();
    await this.engine.validate(managers);
    return managers;
  }

  /**
   * Prepares a fully validated and merged set of provider-ready secret effects.
   *
   * @remarks
   * This is the core orchestration method called by commands like `kubricate secret apply`.
   *
   * @description
   * Executes the full secret orchestration lifecycle:
   * - Validates project configuration and all secret managers
   * - Loads and resolves all secrets across managers
   * - Prepares raw provider effects for each secret
   * - Merges effects according to conflict strategies (intraProvider, crossProvider, etc.)
   *
   * Logs context and important processing steps for debugging and traceability.
   *
   * @returns {Promise<PreparedEffect[]>} A list of finalized secret effects ready for output (e.g., Kubernetes manifests).
   *
   * @throws {Error}
   * - If configuration validation fails (e.g., strictConflictMode violations).
   * - If loading or preparing secrets fails.
   * - If conflict resolution encounters an unrecoverable error (based on config).
   */
  async apply(): Promise<PreparedEffect[]> {
    const managers = await this.validate();

    this.logOrchestratorContext(this.engine.options.config.secret);

    // 1. Load and resolve all secrets
    const resolvedSecrets = await this.loadSecretsFromManagers(managers);

    // 2. Prepare raw effects using each provider
    const rawEffects = this.prepareEffects(resolvedSecrets);

    // 3. Merge grouped effects by provider
    return this.mergePreparedEffects(rawEffects);
  }

  private logOrchestratorContext(mergeOptions: ConfigConflictOptions | undefined): void {
    this.logger.info(`Using merge strategies:`);
    this.logger.info(`  - intraProvider: ${this.resolveStrategyForLevel('intraProvider', mergeOptions)}`);
    this.logger.info(`  - crossManager: ${this.resolveStrategyForLevel('crossManager', mergeOptions)}`);
    this.logger.info(`  - crossProvider: ${this.resolveStrategyForLevel('crossProvider', mergeOptions)}`);
  }

  private async loadSecretsFromManagers(managers: MergedSecretManager): Promise<ResolvedSecret[]> {
    const resolved: ResolvedSecret[] = [];

    for (const entry of Object.values(managers)) {
      const secrets = entry.secretManager.getSecrets();
      const loaded = await this.engine.loadSecrets(entry.secretManager, secrets);

      for (const [key, value] of Object.entries(loaded)) {
        const secretDef = secrets[key];
        resolved.push({
          key,
          value,
          providerName: String(secretDef.provider),
          managerName: entry.name,
        });
      }
    }

    return resolved;
  }

  private prepareEffects(resolvedSecrets: ResolvedSecret[]): PreparedEffectWithMeta[] {
    return resolvedSecrets.flatMap(secret => {
      const provider = this.resolveProviderByName(secret.providerName, secret.managerName);
      const effects = provider.prepare(secret.key, secret.value);

      return effects.map(effect => ({
        ...effect,
        managerName: secret.managerName,
        providerName: provider.name!,
        secretType: provider.secretType ?? provider.constructor.name,
        identifier: provider.getEffectIdentifier?.(effect),
      }));
    });
  }

  /**
   * Resolves the canonical conflict key for a given prepared secret effect.
   *
   * @description
   * This key is used to group and detect conflicts between secrets during the orchestration phase.
   *
   * Normally, the conflict key includes:
   * - `managerName`
   * - `providerClassName` (inferred from secretType)
   * - `identifier` (logical resource name)
   *
   * This ensures strict isolation between different SecretManagers or stacks.
   *
   * However, if the `crossManager` conflict strategy is explicitly configured as `'autoMerge'`,
   * the orchestrator intentionally ignores the `managerName` prefix — allowing secrets from
   * multiple managers to merge into the same logical resource (e.g., Kubernetes Secret, Vault path).
   *
   * ---
   *
   * Example behavior:
   *
   * Default (strict isolation):
   * ```text
   * frontend:Kubricate.OpaqueSecretProvider:app-secret
   * backend:Kubricate.OpaqueSecretProvider:app-secret
   * ```
   * ➔ Different managers, different keys ➔ Conflict detected.
   *
   * CrossManager autoMerge mode:
   * ```text
   * Kubricate.OpaqueSecretProvider:app-secret
   * ```
   * ➔ Same key ➔ Secrets will be merged.
   *
   * ---
   *
   * @param effect - The prepared effect to calculate the conflict key for.
   * @returns The canonical string key used for grouping and conflict resolution.
   */
  private resolveConflictKey(effect: PreparedEffectWithMeta): string {
    return `${effect.secretType}:${effect.identifier}`;
  }

  private mergePreparedEffects(effects: PreparedEffectWithMeta[]): PreparedEffect[] {
    const grouped = new Map<string, PreparedEffectWithMeta[]>();

    for (const effect of effects) {
      const conflictKey = this.resolveConflictKey(effect);
      this.logger.debug(
        `[conflict:group] Grouping "${conflictKey}" from "${effect.managerName}" (${effect.providerName})`
      );
      if (!grouped.has(conflictKey)) grouped.set(conflictKey, []);
      grouped.get(conflictKey)!.push(effect);
    }

    const merged: PreparedEffect[] = [];

    for (const [mergeKey, group] of grouped.entries()) {
      const providerNames = new Set(group.map(e => e.providerName));
      const managerNames = new Set(group.map(e => e.managerName));

      const level: ConflictLevel =
        managerNames.size > 1 ? 'crossManager' : providerNames.size > 1 ? 'crossProvider' : 'intraProvider';

      const strategy = this.resolveStrategyForLevel(level, this.engine.options.config.secret);
      const providerName = group[0].providerName;
      const provider = this.resolveProviderByName(providerName, group[0].managerName);

      // 🔒 Enforce identifier sanity
      if (!provider.getEffectIdentifier && group.length > 1) {
        throw new Error(
          `[conflict:error] Provider "${providerName}" must implement getEffectIdentifier() to safely merge multiple effects (identifier: "${mergeKey}")`
        );
      }

      // 🔒 Enforce provider.allowMerge and strategy
      if (group.length > 1) {
        const sources = formatMergeSources(group);

        if (!provider.allowMerge) {
          throw new Error(
            `[conflict:error] Provider "${providerName}" does not allow merging for identifier "${mergeKey}". Found in:\n  - ${sources.join('\n  - ')}`
          );
        }

        if (strategy === 'error') {
          throw new Error(
            `[conflict:error:${level}] Duplicate resource identifier "${mergeKey}" detected in:\n  - ${sources.join('\n  - ')}`
          );
        }

        if (strategy === 'overwrite') {
          const dropped = sources.slice(0, -1);
          const kept = sources[sources.length - 1];
          this.logger.warn(
            `[conflict:overwrite:${level}] Overwriting "${mergeKey}" — keeping ${kept}, dropped :\n  - ${dropped.join('\n  - ')}`
          );
          group.splice(0, group.length - 1); // keep only the last
        }

        // 'autoMerge' = no-op
      }

      if (typeof provider.mergeSecrets !== 'function') {
        throw new Error(`[conflict:error] Provider "${providerName}" does not implement mergeSecrets()`);
      }

      merged.push(...provider.mergeSecrets(group));
    }

    return merged;
  }

  /**
   * Resolves a provider instance from its name by scanning all SecretManagers.
   * Caches resolved providers for performance.
   *
   * @throws If the provider name is not found in any manager
   */
  private resolveProviderByName(providerName: string, expectedManagerName?: string): BaseProvider {
    const cacheKey = expectedManagerName ? `${expectedManagerName}:${providerName}` : providerName;

    if (this.providerCache.has(cacheKey)) {
      return this.providerCache.get(cacheKey);
    }

    for (const entry of Object.values(this.engine.collect())) {
      const secrets = entry.secretManager.getSecrets();
      const managerName = entry.name;

      this.logger.debug(`[SecretsOrchestrator] Looking for provider "${providerName}" in "${managerName}"`);
      this.logger.debug(`[SecretsOrchestrator] Found secrets: ${JSON.stringify(secrets)}`);

      for (const { provider } of Object.values(secrets)) {
        if (provider === providerName) {
          if (!expectedManagerName || managerName === expectedManagerName) {
            const instance = entry.secretManager.resolveProvider(provider);
            this.providerCache.set(cacheKey, instance);
            return instance;
          }
        }
      }
    }

    throw new Error(
      `[SecretsOrchestrator] Provider "${providerName}" not found in any registered SecretManager${expectedManagerName ? ` "${expectedManagerName}"` : ''}`
    );
  }

  /**
   * Resolves the merge strategy for a given level using config or fallback defaults.
   */
  private resolveStrategyForLevel(
    level: ConflictLevel,
    conflictOptions: ConfigConflictOptions | undefined
  ): ConflictStrategy {
    const strict = conflictOptions?.conflict?.strict ?? false;

    const defaults: Record<ConflictLevel, ConflictStrategy> = strict
      ? {
          intraProvider: 'error', // no merging at all
          crossProvider: 'error',
          crossManager: 'error',
        }
      : {
          intraProvider: 'autoMerge', // default allows merging inside provider
          crossProvider: 'error',
          crossManager: 'error',
        };

    return conflictOptions?.conflict?.strategies?.[level] ?? defaults[level];
  }

  /**
   * Validates core secrets-related configuration inside the project config.
   *
   * @param config - The Kubricate project configuration object.
   *
   * @throws {Error} If the secret manager is missing or invalid.
   */
  private validateConfig(config: KubricateConfig): void {
    // Validate conflict options
    this.validateConflictOptions(config.secret);
  }

  /**
   * Validates conflict resolution options, especially when `strictConflictMode` is enabled.
   *
   * - If `strictConflictMode` is true, all conflict strategies must be set to 'error'.
   * - Throws early if an invalid combination is detected.
   *
   * @param conflictOptions - The secret conflict configuration object.
   *
   * @throws {Error} If strict mode is enabled but a non-'error' strategy is found.
   */
  private validateConflictOptions(conflictOptions: ConfigConflictOptions | undefined) {
    if (!conflictOptions?.conflict?.strict) return;

    for (const [level, strategy] of Object.entries(conflictOptions.conflict?.strategies ?? {})) {
      if (strategy !== 'error') {
        throw new Error(`[config:strictConflictMode] Strategy for "${level}" must be "error" (found "${strategy}").`);
      }
    }
  }
}

function formatMergeSources(group: PreparedEffectWithMeta[]): string[] {
  return group.map(g => {
    const keys = g.secretName ?? 'unknown';
    return `SecretManager: ${g.managerName}, Provider: ${g.providerName}, Keys: [${keys}]`;
  });
}
