/**
 * VaultClient — HashiCorp Vault KV v2 integration for SentryAgent.ai AgentIdP.
 * Manages agent credential secrets in Vault instead of storing bcrypt hashes in PostgreSQL.
 *
 * Vault is optional. When VAULT_ADDR is not set the server operates in
 * bcrypt-only mode (Phase 1 behaviour). VaultClient is only instantiated when
 * all three required env vars are present.
 */

import nodeVault from 'node-vault';
import { CredentialError } from '../utils/errors.js';

/** The single secret field name stored under each KV v2 path. */
const SECRET_FIELD = 'clientSecret';

/** Raw KV v2 read response shape from node-vault. */
interface KvV2ReadResponse {
  data: {
    data: Record<string, string>;
    metadata: {
      version: number;
      destroyed: boolean;
      deletion_time: string;
    };
  };
}

/**
 * Wraps HashiCorp Vault KV v2 operations for credential secret management.
 * All secrets are stored under `{mount}/data/agentidp/agents/{agentId}/credentials/{credentialId}`.
 */
export class VaultClient {
  private readonly client: ReturnType<typeof nodeVault>;
  private readonly mount: string;

  /**
   * @param vaultAddr - Vault server address (e.g. http://127.0.0.1:8200).
   * @param vaultToken - Vault authentication token.
   * @param mount - KV v2 mount path (default: 'secret').
   */
  constructor(vaultAddr: string, vaultToken: string, mount: string = 'secret') {
    this.client = nodeVault({ endpoint: vaultAddr, token: vaultToken });
    this.mount = mount;
  }

  /**
   * Builds the Vault KV v2 data path for a credential.
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @returns Full KV v2 data path, e.g. `secret/data/agentidp/agents/{agentId}/credentials/{credentialId}`.
   */
  private dataPath(agentId: string, credentialId: string): string {
    return `${this.mount}/data/agentidp/agents/${agentId}/credentials/${credentialId}`;
  }

  /**
   * Builds the Vault KV v2 metadata path for a credential (used for permanent deletion).
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @returns Full KV v2 metadata path.
   */
  private metadataPath(agentId: string, credentialId: string): string {
    return `${this.mount}/metadata/agentidp/agents/${agentId}/credentials/${credentialId}`;
  }

  /**
   * Stores a plain-text client secret in Vault for the given credential.
   * Creates or overwrites the secret at the KV v2 path (new version on overwrite).
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @param plainSecret - The plain-text client secret to store.
   * @returns The Vault KV v2 data path where the secret was stored.
   * @throws CredentialError if the Vault write fails.
   */
  async writeSecret(
    agentId: string,
    credentialId: string,
    plainSecret: string,
  ): Promise<string> {
    const path = this.dataPath(agentId, credentialId);
    try {
      await this.client.write(path, { data: { [SECRET_FIELD]: plainSecret } });
    } catch (err) {
      throw new CredentialError(
        `Failed to write credential secret to Vault: ${err instanceof Error ? err.message : String(err)}`,
        'VAULT_WRITE_ERROR',
        { agentId, credentialId },
      );
    }
    return path;
  }

  /**
   * Reads and returns the plain-text client secret from Vault.
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @returns The plain-text client secret.
   * @throws CredentialError if the secret is not found or the read fails.
   */
  async readSecret(agentId: string, credentialId: string): Promise<string> {
    const path = this.dataPath(agentId, credentialId);
    let response: KvV2ReadResponse;
    try {
      response = (await this.client.read(path)) as KvV2ReadResponse;
    } catch (err) {
      throw new CredentialError(
        `Failed to read credential secret from Vault: ${err instanceof Error ? err.message : String(err)}`,
        'VAULT_READ_ERROR',
        { agentId, credentialId },
      );
    }

    const secret = response?.data?.data?.[SECRET_FIELD];
    if (typeof secret !== 'string' || secret.length === 0) {
      throw new CredentialError(
        'Vault returned an empty or missing credential secret.',
        'VAULT_SECRET_MISSING',
        { agentId, credentialId },
      );
    }
    return secret;
  }

  /**
   * Verifies a plain-text secret against the value stored in Vault.
   * Performs a constant-time comparison to prevent timing attacks.
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @param candidateSecret - The plain-text secret to verify.
   * @returns `true` if the secret matches, `false` if it does not.
   */
  async verifySecret(
    agentId: string,
    credentialId: string,
    candidateSecret: string,
  ): Promise<boolean> {
    let stored: string;
    try {
      stored = await this.readSecret(agentId, credentialId);
    } catch {
      return false;
    }

    // Constant-time comparison using crypto.timingSafeEqual
    const { timingSafeEqual } = await import('crypto');
    if (stored.length !== candidateSecret.length) {
      // Still perform a dummy comparison to avoid timing leaks on length differences
      timingSafeEqual(Buffer.from(stored), Buffer.from(stored));
      return false;
    }
    return timingSafeEqual(Buffer.from(stored), Buffer.from(candidateSecret));
  }

  /**
   * Writes a key-value data map to an arbitrary Vault KV v2 data path.
   *
   * Used by subsystems (e.g. webhook secrets) that store secrets at paths
   * outside the standard credential hierarchy.
   *
   * @param dataPath - Full KV v2 data path (e.g. `secret/data/agentidp/webhooks/...`).
   * @param data - Key-value pairs to store under the `data` envelope.
   * @throws CredentialError if the Vault write fails.
   */
  async writeArbitrarySecret(dataPath: string, data: Record<string, string>): Promise<void> {
    try {
      await this.client.write(dataPath, { data });
    } catch (err) {
      throw new CredentialError(
        `Failed to write secret to Vault at path '${dataPath}': ${err instanceof Error ? err.message : String(err)}`,
        'VAULT_WRITE_ERROR',
        { dataPath },
      );
    }
  }

  /**
   * Reads a key-value data map from an arbitrary Vault KV v2 data path.
   *
   * @param dataPath - Full KV v2 data path.
   * @returns The stored key-value data map.
   * @throws CredentialError if the read fails or the path contains no data.
   */
  async readArbitrarySecret(dataPath: string): Promise<Record<string, string>> {
    let response: KvV2ReadResponse;
    try {
      response = (await this.client.read(dataPath)) as KvV2ReadResponse;
    } catch (err) {
      throw new CredentialError(
        `Failed to read secret from Vault at path '${dataPath}': ${err instanceof Error ? err.message : String(err)}`,
        'VAULT_READ_ERROR',
        { dataPath },
      );
    }
    const stored = response?.data?.data;
    if (typeof stored !== 'object' || stored === null) {
      throw new CredentialError(
        'Vault returned an empty or missing data object.',
        'VAULT_SECRET_MISSING',
        { dataPath },
      );
    }
    return stored;
  }

  /**
   * Permanently deletes all versions of a credential secret from Vault.
   * Called on credential revocation.
   *
   * @param agentId - The agent UUID.
   * @param credentialId - The credential UUID.
   * @throws CredentialError if the deletion fails.
   */
  async deleteSecret(agentId: string, credentialId: string): Promise<void> {
    const path = this.metadataPath(agentId, credentialId);
    try {
      await this.client.delete(path);
    } catch (err) {
      throw new CredentialError(
        `Failed to delete credential secret from Vault: ${err instanceof Error ? err.message : String(err)}`,
        'VAULT_DELETE_ERROR',
        { agentId, credentialId },
      );
    }
  }
}

/**
 * Creates a VaultClient from environment variables, or returns null if Vault is not configured.
 * When null is returned, the server operates in bcrypt-only mode (Phase 1 behaviour).
 *
 * Required env vars: VAULT_ADDR, VAULT_TOKEN
 * Optional env var: VAULT_MOUNT (default: 'secret')
 *
 * @returns A configured VaultClient, or null if VAULT_ADDR/VAULT_TOKEN are not set.
 */
export function createVaultClientFromEnv(): VaultClient | null {
  const addr = process.env.VAULT_ADDR;
  const token = process.env.VAULT_TOKEN;

  if (!addr || !token) {
    return null;
  }

  const mount = process.env.VAULT_MOUNT ?? 'secret';
  return new VaultClient(addr, token, mount);
}
