/**
 * A2A Delegation Service for SentryAgent.ai AgentIdP.
 * Business logic for creating, verifying, and revoking delegation chains.
 */

import { Pool, QueryResult } from 'pg';
import { AuditService } from './AuditService.js';
import {
  DelegationChain,
  CreateDelegationRequest,
  DelegationVerificationResult,
  DelegationTokenPayload,
} from '../types/delegation.js';
import {
  signDelegationPayload,
  verifyDelegationSignature,
  generateDelegationToken,
} from '../utils/delegationCrypto.js';
import {
  AgentNotFoundError,
  ValidationError,
  AuthorizationError,
  CredentialAlreadyRevokedError,
} from '../utils/errors.js';

/** Database row shape returned by delegation_chains queries. */
interface DelegationChainRow {
  id: string;
  tenant_id: string;
  delegator_agent_id: string;
  delegatee_agent_id: string;
  scopes: string[];
  delegation_token: string;
  signature: string;
  ttl_seconds: number;
  issued_at: Date;
  expires_at: Date;
  revoked_at: Date | null;
  created_at: Date;
}

/** Interface contract for the delegation service. */
export interface IDelegationService {
  /**
   * Create a delegation chain from delegator to delegatee.
   * Validates scope subset, signs payload, inserts DB row, writes audit log.
   */
  createDelegation(
    tenantId: string,
    delegatorAgentId: string,
    delegatorScopes: string[],
    request: CreateDelegationRequest,
    ipAddress: string,
    userAgent: string,
  ): Promise<DelegationChain>;

  /**
   * Verify a delegation token. Returns chain details with valid flag.
   * Does not throw on expired/revoked — returns valid: false.
   */
  verifyDelegation(
    delegationToken: string,
    agentId: string,
    ipAddress: string,
    userAgent: string,
  ): Promise<DelegationVerificationResult>;

  /**
   * Revoke a delegation chain. Only the delegator may revoke.
   */
  revokeDelegation(
    chainId: string,
    requestingAgentId: string,
    ipAddress: string,
    userAgent: string,
  ): Promise<void>;
}

/**
 * Implementation of IDelegationService.
 * Uses PostgreSQL for persistence and HMAC-SHA256 for delegation payload signing.
 */
export class DelegationService implements IDelegationService {
  private readonly delegationSecret: string;

  /**
   * @param pool - PostgreSQL connection pool.
   * @param auditService - Audit log service for recording delegation events.
   */
  constructor(
    private readonly pool: Pool,
    private readonly auditService: AuditService,
  ) {
    this.delegationSecret =
      process.env['DELEGATION_SECRET'] ?? process.env['JWT_SECRET'] ?? 'delegation-secret-change-me';
  }

  /**
   * Creates a delegation chain, signing the payload and storing it in the database.
   *
   * @param tenantId - The organization ID of the delegating agent.
   * @param delegatorAgentId - UUID of the agent granting delegation.
   * @param delegatorScopes - Scopes held by the delegator (from their JWT).
   * @param request - Delegation request body.
   * @param ipAddress - Caller IP for audit log.
   * @param userAgent - Caller user-agent for audit log.
   * @returns The created DelegationChain record.
   * @throws ValidationError on invalid scopes, ttl, or self-delegation.
   * @throws AgentNotFoundError if delegateeAgentId does not exist in the same tenant.
   */
  async createDelegation(
    tenantId: string,
    delegatorAgentId: string,
    delegatorScopes: string[],
    request: CreateDelegationRequest,
    ipAddress: string,
    userAgent: string,
  ): Promise<DelegationChain> {
    const { delegateeAgentId, scopes, ttlSeconds } = request;

    // Validate ttl range
    if (ttlSeconds < 60 || ttlSeconds > 86400) {
      throw new ValidationError('ttlSeconds must be between 60 and 86400.', { code: 'INVALID_TTL' });
    }

    // Reject self-delegation
    if (delegateeAgentId === delegatorAgentId) {
      throw new ValidationError('An agent cannot delegate to itself.', { code: 'SELF_DELEGATION' });
    }

    // Validate scopes are a strict subset of the delegator's own scopes
    const disallowedScopes = scopes.filter((s) => !delegatorScopes.includes(s));
    if (disallowedScopes.length > 0) {
      throw new ValidationError(
        `Requested scopes exceed delegator permissions: ${disallowedScopes.join(', ')}`,
        { code: 'INVALID_SCOPES', disallowedScopes },
      );
    }

    // Verify delegatee exists in the same tenant
    const delegateeResult: QueryResult<{ agent_id: string }> = await this.pool.query(
      `SELECT agent_id FROM agents WHERE agent_id = $1 AND organization_id = $2 AND status = 'active'`,
      [delegateeAgentId, tenantId],
    );
    if (delegateeResult.rows.length === 0) {
      throw new AgentNotFoundError(delegateeAgentId);
    }

    const now = new Date();
    const expiresAt = new Date(now.getTime() + ttlSeconds * 1000);
    const chainId = generateDelegationToken(); // UUID for the chain id
    const delegationToken = generateDelegationToken(); // UUID for the token value

    const payload: DelegationTokenPayload = {
      chainId,
      tenantId,
      delegatorAgentId,
      delegateeAgentId,
      scopes,
      issuedAt: now.toISOString(),
      expiresAt: expiresAt.toISOString(),
    };

    const signature = signDelegationPayload(payload, this.delegationSecret);

    const insertResult: QueryResult<DelegationChainRow> = await this.pool.query(
      `INSERT INTO delegation_chains
         (id, tenant_id, delegator_agent_id, delegatee_agent_id, scopes,
          delegation_token, signature, ttl_seconds, issued_at, expires_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
       RETURNING *`,
      [
        chainId,
        tenantId,
        delegatorAgentId,
        delegateeAgentId,
        scopes,
        delegationToken,
        signature,
        ttlSeconds,
        now,
        expiresAt,
      ],
    );

    await this.auditService.logEvent(
      delegatorAgentId,
      'delegation.created',
      'success',
      ipAddress,
      userAgent,
      { chainId, delegateeAgentId, scopes, ttlSeconds },
      tenantId,
    );

    return this.rowToChain(insertResult.rows[0]);
  }

  /**
   * Verifies a delegation token. Returns valid: false for expired or revoked chains
   * rather than throwing an error.
   *
   * @param delegationToken - The delegation token to verify.
   * @param agentId - The agent performing verification (for audit log).
   * @param ipAddress - Caller IP for audit log.
   * @param userAgent - Caller user-agent for audit log.
   * @returns DelegationVerificationResult with valid flag.
   * @throws AgentNotFoundError if no chain is found for the given token.
   */
  async verifyDelegation(
    delegationToken: string,
    agentId: string,
    ipAddress: string,
    userAgent: string,
  ): Promise<DelegationVerificationResult> {
    const result: QueryResult<DelegationChainRow> = await this.pool.query(
      `SELECT * FROM delegation_chains WHERE delegation_token = $1`,
      [delegationToken],
    );

    if (result.rows.length === 0) {
      throw new AgentNotFoundError(delegationToken);
    }

    const row = result.rows[0];
    const now = new Date();

    const payload: DelegationTokenPayload = {
      chainId: row.id,
      tenantId: row.tenant_id,
      delegatorAgentId: row.delegator_agent_id,
      delegateeAgentId: row.delegatee_agent_id,
      scopes: row.scopes,
      issuedAt: row.issued_at.toISOString(),
      expiresAt: row.expires_at.toISOString(),
    };

    const signatureValid = verifyDelegationSignature(payload, row.signature, this.delegationSecret);
    const notExpired = row.expires_at > now;
    const notRevoked = row.revoked_at === null;
    const valid = signatureValid && notExpired && notRevoked;

    await this.auditService.logEvent(
      agentId,
      'delegation.verified',
      'success',
      ipAddress,
      userAgent,
      { chainId: row.id, valid, signatureValid, notExpired, notRevoked },
      row.tenant_id,
    );

    return {
      valid,
      chainId: row.id,
      delegatorAgentId: row.delegator_agent_id,
      delegateeAgentId: row.delegatee_agent_id,
      scopes: row.scopes,
      issuedAt: row.issued_at,
      expiresAt: row.expires_at,
      revokedAt: row.revoked_at,
    };
  }

  /**
   * Revokes a delegation chain. Only the original delegator may revoke.
   *
   * @param chainId - UUID of the delegation chain to revoke.
   * @param requestingAgentId - The agent attempting revocation (must be the delegator).
   * @param ipAddress - Caller IP for audit log.
   * @param userAgent - Caller user-agent for audit log.
   * @throws AgentNotFoundError if no chain with this ID exists.
   * @throws AuthorizationError if the requesting agent is not the delegator.
   * @throws CredentialAlreadyRevokedError if the chain is already revoked.
   */
  async revokeDelegation(
    chainId: string,
    requestingAgentId: string,
    ipAddress: string,
    userAgent: string,
  ): Promise<void> {
    const result: QueryResult<DelegationChainRow> = await this.pool.query(
      `SELECT * FROM delegation_chains WHERE id = $1`,
      [chainId],
    );

    if (result.rows.length === 0) {
      throw new AgentNotFoundError(chainId);
    }

    const row = result.rows[0];

    if (row.delegator_agent_id !== requestingAgentId) {
      throw new AuthorizationError('Only the delegating agent may revoke this delegation chain.');
    }

    if (row.revoked_at !== null) {
      throw new CredentialAlreadyRevokedError(chainId, row.revoked_at.toISOString());
    }

    await this.pool.query(
      `UPDATE delegation_chains SET revoked_at = NOW() WHERE id = $1`,
      [chainId],
    );

    await this.auditService.logEvent(
      requestingAgentId,
      'delegation.revoked',
      'success',
      ipAddress,
      userAgent,
      { chainId, delegateeAgentId: row.delegatee_agent_id },
      row.tenant_id,
    );
  }

  /** Maps a database row to a DelegationChain domain object. */
  private rowToChain(row: DelegationChainRow): DelegationChain {
    return {
      id: row.id,
      tenantId: row.tenant_id,
      delegatorAgentId: row.delegator_agent_id,
      delegateeAgentId: row.delegatee_agent_id,
      scopes: row.scopes,
      delegationToken: row.delegation_token,
      signature: row.signature,
      ttlSeconds: row.ttl_seconds,
      issuedAt: row.issued_at,
      expiresAt: row.expires_at,
      revokedAt: row.revoked_at,
      createdAt: row.created_at,
    };
  }
}
