/**
 * Credential Repository for SentryAgent.ai AgentIdP.
 * All SQL queries for the credentials table live exclusively here.
 */

import { Pool, QueryResult } from 'pg';
import { v4 as uuidv4 } from 'uuid';
import { ICredential, ICredentialRow, ICredentialListFilters } from '../types/index.js';

/** Raw database row for a credential. */
interface CredentialDbRow {
  credential_id: string;
  client_id: string;
  secret_hash: string;
  vault_path: string | null;
  status: string;
  created_at: Date;
  expires_at: Date | null;
  revoked_at: Date | null;
}

/**
 * Maps a raw database row to the ICredentialRow domain model.
 *
 * @param row - Raw row from the credentials table.
 * @returns Typed ICredentialRow including the secret hash.
 */
function mapRowToCredentialRow(row: CredentialDbRow): ICredentialRow {
  return {
    credentialId: row.credential_id,
    clientId: row.client_id,
    secretHash: row.secret_hash,
    vaultPath: row.vault_path ?? null,
    status: row.status as ICredential['status'],
    createdAt: row.created_at,
    expiresAt: row.expires_at,
    revokedAt: row.revoked_at,
  };
}

/**
 * Maps a raw database row to the ICredential domain model (no secret hash).
 *
 * @param row - Raw row from the credentials table.
 * @returns Typed ICredential without the secret hash.
 */
function mapRowToCredential(row: CredentialDbRow): ICredential {
  const { secretHash: _secretHash, ...credential } = mapRowToCredentialRow(row);
  void _secretHash;
  return credential;
}

/**
 * Repository for all credential database operations.
 * Receives a pg.Pool via constructor injection.
 */
export class CredentialRepository {
  /**
   * @param pool - The PostgreSQL connection pool.
   */
  constructor(private readonly pool: Pool) {}

  /**
   * Creates a new credential record using bcrypt secret hash (Phase 1 / Vault-not-configured).
   *
   * @param clientId - The agent ID this credential belongs to.
   * @param secretHash - The bcrypt hash of the plain-text secret.
   * @param expiresAt - Optional expiry date.
   * @returns The created credential record (without secret hash).
   */
  async create(
    clientId: string,
    secretHash: string,
    expiresAt: Date | null,
  ): Promise<ICredential> {
    const credentialId = uuidv4();
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      `INSERT INTO credentials
         (credential_id, client_id, secret_hash, vault_path, status, created_at, expires_at)
       VALUES ($1, $2, $3, NULL, 'active', NOW(), $4)
       RETURNING *`,
      [credentialId, clientId, secretHash, expiresAt],
    );
    return mapRowToCredential(result.rows[0]);
  }

  /**
   * Creates a new credential record backed by Vault (Phase 2).
   * Accepts a caller-supplied credentialId so the Vault path can include it before the DB write.
   *
   * @param credentialId - The UUID to use for this credential (caller-generated).
   * @param clientId - The agent ID this credential belongs to.
   * @param vaultPath - The Vault KV v2 data path where the secret is stored.
   * @param expiresAt - Optional expiry date.
   * @returns The created credential record.
   */
  async createWithVaultPath(
    credentialId: string,
    clientId: string,
    vaultPath: string,
    expiresAt: Date | null,
  ): Promise<ICredential> {
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      `INSERT INTO credentials
         (credential_id, client_id, secret_hash, vault_path, status, created_at, expires_at)
       VALUES ($1, $2, '', $3, 'active', NOW(), $4)
       RETURNING *`,
      [credentialId, clientId, vaultPath, expiresAt],
    );
    return mapRowToCredential(result.rows[0]);
  }

  /**
   * Finds a credential by its UUID, including the secret hash.
   *
   * @param credentialId - The credential UUID.
   * @returns The credential row including secret hash, or null if not found.
   */
  async findById(credentialId: string): Promise<ICredentialRow | null> {
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      'SELECT * FROM credentials WHERE credential_id = $1',
      [credentialId],
    );
    if (result.rows.length === 0) return null;
    return mapRowToCredentialRow(result.rows[0]);
  }

  /**
   * Returns a paginated list of credentials for an agent.
   *
   * @param agentId - The agent UUID.
   * @param filters - Pagination and optional status filter.
   * @returns Object with credential list and total count.
   */
  async findByAgentId(
    agentId: string,
    filters: ICredentialListFilters,
  ): Promise<{ credentials: ICredential[]; total: number }> {
    const conditions: string[] = ['client_id = $1'];
    const params: unknown[] = [agentId];
    let paramIndex = 2;

    if (filters.status !== undefined) {
      conditions.push(`status = $${paramIndex++}`);
      params.push(filters.status);
    }

    const whereClause = `WHERE ${conditions.join(' AND ')}`;

    const countResult: QueryResult<{ count: string }> = await this.pool.query(
      `SELECT COUNT(*) as count FROM credentials ${whereClause}`,
      params,
    );
    const total = parseInt(countResult.rows[0].count, 10);

    const offset = (filters.page - 1) * filters.limit;
    const dataParams = [...params, filters.limit, offset];

    const dataResult: QueryResult<CredentialDbRow> = await this.pool.query(
      `SELECT * FROM credentials ${whereClause}
       ORDER BY created_at DESC
       LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
      dataParams,
    );

    return {
      credentials: dataResult.rows.map(mapRowToCredential),
      total,
    };
  }

  /**
   * Updates the bcrypt hash for an existing credential (rotation, bcrypt path).
   *
   * @param credentialId - The credential UUID.
   * @param newSecretHash - The new bcrypt hash.
   * @param newExpiresAt - Optional new expiry date.
   * @returns The updated credential record, or null if not found.
   */
  async updateHash(
    credentialId: string,
    newSecretHash: string,
    newExpiresAt: Date | null,
  ): Promise<ICredential | null> {
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      `UPDATE credentials
       SET secret_hash = $1, vault_path = NULL, expires_at = $2, status = 'active', revoked_at = NULL
       WHERE credential_id = $3
       RETURNING *`,
      [newSecretHash, newExpiresAt, credentialId],
    );
    if (result.rows.length === 0) return null;
    return mapRowToCredential(result.rows[0]);
  }

  /**
   * Updates the vault_path for an existing credential (rotation, Vault path).
   *
   * @param credentialId - The credential UUID.
   * @param newVaultPath - The new Vault KV v2 data path.
   * @param newExpiresAt - Optional new expiry date.
   * @returns The updated credential record, or null if not found.
   */
  async updateVaultPath(
    credentialId: string,
    newVaultPath: string,
    newExpiresAt: Date | null,
  ): Promise<ICredential | null> {
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      `UPDATE credentials
       SET vault_path = $1, secret_hash = '', expires_at = $2, status = 'active', revoked_at = NULL
       WHERE credential_id = $3
       RETURNING *`,
      [newVaultPath, newExpiresAt, credentialId],
    );
    if (result.rows.length === 0) return null;
    return mapRowToCredential(result.rows[0]);
  }

  /**
   * Sets a credential's status to 'revoked'.
   *
   * @param credentialId - The credential UUID.
   * @returns The updated credential record, or null if not found.
   */
  async revoke(credentialId: string): Promise<ICredential | null> {
    const result: QueryResult<CredentialDbRow> = await this.pool.query(
      `UPDATE credentials
       SET status = 'revoked', revoked_at = NOW()
       WHERE credential_id = $1
       RETURNING *`,
      [credentialId],
    );
    if (result.rows.length === 0) return null;
    return mapRowToCredential(result.rows[0]);
  }

  /**
   * Revokes all active credentials for an agent (used on decommission).
   *
   * @param agentId - The agent UUID.
   * @returns The number of credentials revoked.
   */
  async revokeAllForAgent(agentId: string): Promise<number> {
    const result = await this.pool.query(
      `UPDATE credentials
       SET status = 'revoked', revoked_at = NOW()
       WHERE client_id = $1 AND status = 'active'`,
      [agentId],
    );
    return result.rowCount ?? 0;
  }

  /**
   * Finds the client_id of the most recent active credential for an agent.
   *
   * @param agentId - The agent UUID.
   * @returns The client_id string, or null if no active credential exists.
   */
  async findActiveClientId(agentId: string): Promise<string | null> {
    const result: QueryResult<{ client_id: string }> = await this.pool.query(
      `SELECT client_id FROM credentials WHERE agent_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`,
      [agentId],
    );
    return result.rows.length > 0 ? (result.rows[0].client_id ?? null) : null;
  }
}
