import {
  Inject,
  Injectable,
  Logger,
  type OnModuleInit,
  type OnModuleDestroy,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { EventEmitter2 } from "@nestjs/event-emitter";

import { VAULT_MODULE_OPTIONS, VAULT_REFRESH_EVENT } from "./vault.constants.js";
import { VaultClient } from "./vault.client.js";
import type { VaultConfig, VaultTokenState } from "./vault.types.js";

const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
const DEFAULT_TOKEN_RENEW_THRESHOLD = 0.75;

@Injectable()
export class VaultService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(VaultService.name);
  private readonly client: VaultClient;
  private readonly refreshIntervalMs: number;
  private readonly tokenRenewThreshold: number;

  private tokenState: VaultTokenState | null = null;
  private lastKvVersion: number | null = null;
  private refreshTimer: ReturnType<typeof setInterval> | null = null;
  private renewTimer: ReturnType<typeof setTimeout> | null = null;

  constructor(
    @Inject(VAULT_MODULE_OPTIONS) private readonly config: VaultConfig,
    private readonly configService: ConfigService,
    private readonly eventEmitter: EventEmitter2
  ) {
    this.client = new VaultClient(config.address, config.requestTimeoutMs);
    this.refreshIntervalMs = config.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
    this.tokenRenewThreshold = config.tokenRenewThreshold ?? DEFAULT_TOKEN_RENEW_THRESHOLD;
  }

  async onModuleInit(): Promise<void> {
    await this.authenticate();
    await this.fetchAndApplySecrets();
    this.startRefreshLoop();
    this.scheduleTokenRenewal();
    this.logger.log("Vault secrets loaded and refresh loop started");
  }

  onModuleDestroy(): void {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer);
      this.refreshTimer = null;
    }
    if (this.renewTimer) {
      clearTimeout(this.renewTimer);
      this.renewTimer = null;
    }
  }

  private async authenticate(): Promise<void> {
    const response = await this.client.login(this.config.roleId, this.config.secretId);
    this.tokenState = {
      clientToken: response.auth.client_token,
      leaseDuration: response.auth.lease_duration,
      authenticatedAt: Date.now(),
    };
    this.logger.log(`Authenticated with Vault (TTL: ${response.auth.lease_duration}s)`);
  }

  private async fetchAndApplySecrets(): Promise<boolean> {
    if (!this.tokenState) {
      throw new Error("Vault: not authenticated");
    }

    const response = await this.client.readSecret(
      this.config.secretPath,
      this.tokenState.clientToken
    );

    const currentVersion = response.data.metadata.version;

    if (this.lastKvVersion !== null && currentVersion === this.lastKvVersion) {
      return false;
    }

    const secrets = response.data.data;
    const changedKeys: string[] = [];

    for (const [key, value] of Object.entries(secrets)) {
      const current = this.configService.get<string>(key);
      if (current !== value) {
        changedKeys.push(key);
        // Update process.env so ConfigService picks up the new value
        process.env[key] = value;
      }
    }

    this.lastKvVersion = currentVersion;

    if (changedKeys.length > 0) {
      this.logger.log(`Vault secrets updated (v${currentVersion}): ${changedKeys.join(", ")}`);
      this.eventEmitter.emit(VAULT_REFRESH_EVENT, { changedKeys });
      return true;
    }

    return false;
  }

  private startRefreshLoop(): void {
    this.refreshTimer = setInterval(() => {
      void this.refreshSecrets();
    }, this.refreshIntervalMs);
  }

  private async refreshSecrets(): Promise<void> {
    try {
      await this.fetchAndApplySecrets();
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Vault refresh failed, keeping cached values: ${message}`);
    }
  }

  private scheduleTokenRenewal(): void {
    if (!this.tokenState) return;

    const renewAfterMs = this.tokenState.leaseDuration * 1000 * this.tokenRenewThreshold;

    this.renewTimer = setTimeout(() => {
      void this.renewToken();
    }, renewAfterMs);
  }

  private async renewToken(): Promise<void> {
    if (!this.tokenState) return;

    try {
      const response = await this.client.renewToken(this.tokenState.clientToken);
      this.tokenState = {
        clientToken: response.auth.client_token,
        leaseDuration: response.auth.lease_duration,
        authenticatedAt: Date.now(),
      };
      this.logger.log(`Vault token renewed (TTL: ${response.auth.lease_duration}s)`);
      this.scheduleTokenRenewal();
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Vault token renewal failed, re-authenticating: ${message}`);
      try {
        await this.authenticate();
        this.scheduleTokenRenewal();
      } catch (authError) {
        const authMessage = authError instanceof Error ? authError.message : String(authError);
        this.logger.error(`Vault re-authentication failed: ${authMessage}`);
      }
    }
  }
}
