/**
 * Federation grants service — CRUD + status transitions (FED-M2-06).
 *
 * Business logic only. CSR/cert work is handled by M2-07.
 *
 * Status lifecycle:
 *   pending → active   (activateGrant, called by M2-07 enrollment controller after cert signed)
 *   active  → revoked  (revokeGrant)
 *   active  → expired  (expireGrant, called by M6 scheduler)
 */

import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { type Db, and, eq, federationGrants, federationPeers } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import { parseFederationScope } from './scope-schema.js';
import type { CreateGrantDto, ListGrantsDto } from './grants.dto.js';

export type Grant = typeof federationGrants.$inferSelect;
export type Peer = typeof federationPeers.$inferSelect;
export type GrantWithPeer = Grant & { peer: Peer };

@Injectable()
export class GrantsService {
  constructor(@Inject(DB) private readonly db: Db) {}

  /**
   * Create a new grant in `pending` state.
   * Validates the scope against the federation scope JSON schema before inserting.
   */
  async createGrant(dto: CreateGrantDto): Promise<Grant> {
    // Throws FederationScopeError (a plain Error subclass) on invalid scope.
    parseFederationScope(dto.scope);

    const [grant] = await this.db
      .insert(federationGrants)
      .values({
        peerId: dto.peerId,
        subjectUserId: dto.subjectUserId,
        scope: dto.scope,
        status: 'pending',
        expiresAt: dto.expiresAt != null ? new Date(dto.expiresAt) : null,
      })
      .returning();

    return grant!;
  }

  /**
   * Fetch a single grant by ID. Throws NotFoundException if not found.
   */
  async getGrant(id: string): Promise<Grant> {
    const [grant] = await this.db
      .select()
      .from(federationGrants)
      .where(eq(federationGrants.id, id))
      .limit(1);

    if (!grant) {
      throw new NotFoundException(`Grant ${id} not found`);
    }

    return grant;
  }

  /**
   * Fetch a single grant by ID, joined with its associated peer row.
   * Used by FederationAuthGuard to perform grant status + cert serial checks
   * in a single DB round-trip.
   *
   * Throws NotFoundException if the grant does not exist.
   * Throws NotFoundException if the associated peer row is missing (data integrity issue).
   */
  async getGrantWithPeer(id: string): Promise<GrantWithPeer> {
    const rows = await this.db
      .select()
      .from(federationGrants)
      .innerJoin(federationPeers, eq(federationGrants.peerId, federationPeers.id))
      .where(eq(federationGrants.id, id))
      .limit(1);

    const row = rows[0];
    if (!row) {
      throw new NotFoundException(`Grant ${id} not found`);
    }

    return {
      ...row.federation_grants,
      peer: row.federation_peers,
    };
  }

  /**
   * List grants with optional filters for peerId, subjectUserId, and status.
   */
  async listGrants(filters: ListGrantsDto): Promise<Grant[]> {
    const conditions = [];

    if (filters.peerId != null) {
      conditions.push(eq(federationGrants.peerId, filters.peerId));
    }
    if (filters.subjectUserId != null) {
      conditions.push(eq(federationGrants.subjectUserId, filters.subjectUserId));
    }
    if (filters.status != null) {
      conditions.push(eq(federationGrants.status, filters.status));
    }

    if (conditions.length === 0) {
      return this.db.select().from(federationGrants);
    }

    return this.db
      .select()
      .from(federationGrants)
      .where(and(...conditions));
  }

  /**
   * Transition a grant from `pending` → `active`.
   * Called by M2-07 enrollment controller after cert is signed.
   * Throws ConflictException if the grant is not in `pending` state.
   */
  async activateGrant(id: string): Promise<Grant> {
    const grant = await this.getGrant(id);

    if (grant.status !== 'pending') {
      throw new ConflictException(
        `Grant ${id} cannot be activated: expected status 'pending', got '${grant.status}'`,
      );
    }

    const [updated] = await this.db
      .update(federationGrants)
      .set({ status: 'active' })
      .where(eq(federationGrants.id, id))
      .returning();

    return updated!;
  }

  /**
   * Transition a grant from `active` → `revoked`.
   * Sets revokedAt and optionally revokedReason.
   * Throws ConflictException if the grant is not in `active` state.
   */
  async revokeGrant(id: string, reason?: string): Promise<Grant> {
    const grant = await this.getGrant(id);

    if (grant.status !== 'active') {
      throw new ConflictException(
        `Grant ${id} cannot be revoked: expected status 'active', got '${grant.status}'`,
      );
    }

    const [updated] = await this.db
      .update(federationGrants)
      .set({
        status: 'revoked',
        revokedAt: new Date(),
        revokedReason: reason ?? null,
      })
      .where(eq(federationGrants.id, id))
      .returning();

    return updated!;
  }

  /**
   * Transition a grant from `active` → `expired`.
   * Intended for use by the M6 scheduler.
   * Throws ConflictException if the grant is not in `active` state.
   */
  async expireGrant(id: string): Promise<Grant> {
    const grant = await this.getGrant(id);

    if (grant.status !== 'active') {
      throw new ConflictException(
        `Grant ${id} cannot be expired: expected status 'active', got '${grant.status}'`,
      );
    }

    const [updated] = await this.db
      .update(federationGrants)
      .set({ status: 'expired' })
      .where(eq(federationGrants.id, id))
      .returning();

    return updated!;
  }
}
