/**
 * OIDCTrustPolicyService — manages GitHub OIDC trust policies for token exchange.
 *
 * Trust policies control which GitHub repositories (and optionally branches) are
 * permitted to exchange a GitHub OIDC token for a SentryAgent.ai agent access token.
 *
 * Only workflows matching a registered trust policy are allowed through
 * the POST /oidc/token endpoint when provider is "github".
 */

import { Pool } from 'pg';
import { SentryAgentError, ValidationError } from '../utils/errors.js';
import {
  ICreateTrustPolicyRequest,
  IOIDCTrustPolicy,
  OIDCProvider,
} from '../types/oidc.js';

// ─────────────────────────────────────────────────────────────────────────────
// Error classes
// ─────────────────────────────────────────────────────────────────────────────

/**
 * 404 — No trust policy exists matching the given criteria.
 */
export class TrustPolicyNotFoundError extends SentryAgentError {
  constructor(id?: string) {
    super(
      'OIDC trust policy not found.',
      'TRUST_POLICY_NOT_FOUND',
      404,
      id ? { id } : undefined,
    );
  }
}

/**
 * 403 — No trust policy permits the token exchange from the given repository/branch.
 */
export class TrustPolicyViolationError extends SentryAgentError {
  constructor(provider: string, repository: string, branch?: string) {
    super(
      `No trust policy permits OIDC token exchange from provider "${provider}", ` +
        `repository "${repository}"` +
        (branch ? `, branch "${branch}"` : '') +
        '. Register a trust policy via POST /oidc/trust-policies.',
      'TRUST_POLICY_VIOLATION',
      403,
      { provider, repository, branch },
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// GitHub OIDC token claims (subset we validate against the trust policy)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Relevant claims from a decoded GitHub OIDC JWT.
 * GitHub populates these in the `sub` claim and as standalone claims.
 * See: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
 */
export interface IGitHubOIDCClaims {
  /** e.g. "repo:org/repo:ref:refs/heads/main" */
  sub: string;
  /** GitHub repository in "org/repo" format */
  repository: string;
  /** e.g. "refs/heads/main" */
  ref?: string;
  /** The Git ref type, e.g. "branch" */
  ref_type?: string;
  /** Workflow run attempt number */
  run_attempt?: string;
}

// ─────────────────────────────────────────────────────────────────────────────
// Repository row shape from DB
// ─────────────────────────────────────────────────────────────────────────────

interface ITrustPolicyRow {
  id: string;
  provider: string;
  repository: string;
  branch: string | null;
  agent_id: string;
  created_at: Date;
  updated_at: Date;
}

/**
 * Service for creating and enforcing GitHub OIDC trust policies.
 */
export class OIDCTrustPolicyService {
  /**
   * @param pool - PostgreSQL connection pool.
   */
  constructor(private readonly pool: Pool) {}

  // ─────────────────────────────────────────────────────────────────────────
  // Public methods
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Registers a new OIDC trust policy.
   *
   * Validates that the referenced agent exists before persisting.
   * A given repository + branch + agent combination may have at most one policy
   * (duplicates are permitted from the DB perspective but lead to redundant entries).
   *
   * @param request - The trust policy creation request.
   * @returns The persisted trust policy.
   * @throws ValidationError if the request is invalid.
   * @throws Error if the database operation fails.
   */
  async createTrustPolicy(request: ICreateTrustPolicyRequest): Promise<IOIDCTrustPolicy> {
    this.validateCreateRequest(request);

    const { provider, repository, branch, agentId } = request;

    // Verify the referenced agent exists
    const agentCheck = await this.pool.query<{ agent_id: string }>(
      'SELECT agent_id FROM agents WHERE agent_id = $1 AND status != $2',
      [agentId, 'decommissioned'],
    );
    if (agentCheck.rowCount === 0) {
      throw new ValidationError(`Agent with ID "${agentId}" was not found or is decommissioned.`, {
        agentId,
      });
    }

    const result = await this.pool.query<ITrustPolicyRow>(
      `INSERT INTO oidc_trust_policies (provider, repository, branch, agent_id)
       VALUES ($1, $2, $3, $4)
       RETURNING id, provider, repository, branch, agent_id, created_at, updated_at`,
      [provider, repository, branch ?? null, agentId],
    );

    return this.mapRow(result.rows[0]);
  }

  /**
   * Lists all trust policies for a given agent.
   *
   * @param agentId - UUID of the agent.
   * @returns Array of trust policies; empty array if none exist.
   */
  async listTrustPoliciesForAgent(agentId: string): Promise<IOIDCTrustPolicy[]> {
    const result = await this.pool.query<ITrustPolicyRow>(
      `SELECT id, provider, repository, branch, agent_id, created_at, updated_at
       FROM oidc_trust_policies
       WHERE agent_id = $1
       ORDER BY created_at DESC`,
      [agentId],
    );
    return result.rows.map((row) => this.mapRow(row));
  }

  /**
   * Deletes a trust policy by its UUID.
   *
   * @param id - UUID of the trust policy to delete.
   * @throws TrustPolicyNotFoundError if no policy with that ID exists.
   */
  async deleteTrustPolicy(id: string): Promise<void> {
    const result = await this.pool.query(
      'DELETE FROM oidc_trust_policies WHERE id = $1',
      [id],
    );
    if (result.rowCount === 0) {
      throw new TrustPolicyNotFoundError(id);
    }
  }

  /**
   * Enforces trust policy on a GitHub OIDC token exchange request.
   *
   * Looks up all trust policies for the given (provider, repository) pair.
   * If a branch constraint is present on a policy, it must match the token's branch.
   * A policy with no branch constraint permits any branch.
   *
   * @param provider - OIDC provider (e.g. "github").
   * @param repository - Repository from the GitHub OIDC token (e.g. "org/repo").
   * @param branch - Branch from the GitHub OIDC token (e.g. "refs/heads/main"), or undefined.
   * @param agentId - UUID of the agent for which the token is being issued.
   * @throws TrustPolicyViolationError if no matching trust policy is found.
   */
  async enforceTrustPolicy(
    provider: OIDCProvider,
    repository: string,
    branch: string | undefined,
    agentId: string,
  ): Promise<void> {
    const result = await this.pool.query<ITrustPolicyRow>(
      `SELECT id, provider, repository, branch, agent_id, created_at, updated_at
       FROM oidc_trust_policies
       WHERE provider = $1
         AND repository = $2
         AND agent_id = $3
       ORDER BY created_at DESC`,
      [provider, repository, agentId],
    );

    if (result.rowCount === 0) {
      throw new TrustPolicyViolationError(provider, repository, branch);
    }

    // At least one policy must match: either branch is null (any branch) or matches exactly
    const normalizedBranch = branch ? this.normalizeBranch(branch) : undefined;

    const matchingPolicy = result.rows.find((row) => {
      if (row.branch === null) {
        // Wildcard — any branch is permitted
        return true;
      }
      if (normalizedBranch === undefined) {
        // Token has no branch claim but policy requires a specific branch
        return false;
      }
      return row.branch === normalizedBranch || row.branch === branch;
    });

    if (!matchingPolicy) {
      throw new TrustPolicyViolationError(provider, repository, branch);
    }
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Private helpers
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Validates a trust policy creation request.
   * Throws ValidationError on any invalid field.
   *
   * @param request - The request to validate.
   */
  private validateCreateRequest(request: ICreateTrustPolicyRequest): void {
    if (request.provider !== 'github') {
      throw new ValidationError(
        'Only "github" is supported as an OIDC provider at this time.',
        { provider: request.provider },
      );
    }

    const repoPattern = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
    if (!repoPattern.test(request.repository)) {
      throw new ValidationError(
        'repository must be in "org/repo" format (e.g. "acme/my-repo").',
        { repository: request.repository },
      );
    }

    if (!request.agentId || request.agentId.trim().length === 0) {
      throw new ValidationError('agentId is required.', { agentId: request.agentId });
    }
  }

  /**
   * Normalises a Git ref to a short branch name.
   * "refs/heads/main" → "main"
   * "main" → "main"
   *
   * @param ref - The raw Git ref string.
   * @returns Normalized branch name.
   */
  private normalizeBranch(ref: string): string {
    return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref;
  }

  /**
   * Maps a database row to an IOIDCTrustPolicy object.
   *
   * @param row - Raw database row.
   * @returns Typed trust policy object.
   */
  private mapRow(row: ITrustPolicyRow): IOIDCTrustPolicy {
    return {
      id: row.id,
      provider: row.provider as OIDCProvider,
      repository: row.repository,
      branch: row.branch,
      agentId: row.agent_id,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
    };
  }
}
