/**
 * ApiKeys Service
 *
 * Manages API key lifecycle:
 * - Generation (GOLD/PLATINIUM subscribers only)
 * - Listing (masked — prefix only)
 * - Revocation
 * - Validation for inbound API key authentication
 */

import { ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';

import { ApiKey } from '@domain/entities/api-key.entity';
import { ApiKeyRepository, API_KEY_REPOSITORY } from '@domain/ports/out/api-key.repository';
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import {
  SubscriptionRepository,
  SUBSCRIPTION_REPOSITORY,
} from '@domain/ports/out/subscription.repository';

import { CreateApiKeyDto, ApiKeyDto, CreateApiKeyResultDto } from '../dto/api-key.dto';

/** Shape of request.user populated when an API key is used. */
export interface ApiKeyUserContext {
  id: string;
  email: string;
  role: string;
  organizationId: string;
  firstName: string;
  lastName: string;
  plan: string;
  planFeatures: string[];
}

const KEY_PREFIX_DISPLAY_LENGTH = 18; // "xped_live_" (10) + 8 hex chars

@Injectable()
export class ApiKeysService {
  private readonly logger = new Logger(ApiKeysService.name);

  constructor(
    @Inject(API_KEY_REPOSITORY)
    private readonly apiKeyRepository: ApiKeyRepository,
    @Inject(USER_REPOSITORY)
    private readonly userRepository: UserRepository,
    @Inject(SUBSCRIPTION_REPOSITORY)
    private readonly subscriptionRepository: SubscriptionRepository
  ) {}

  /**
   * Generate a new API key for the given user / organisation.
   * The full raw key is returned exactly once — it is never persisted.
   */
  async generateApiKey(
    userId: string,
    organizationId: string,
    dto: CreateApiKeyDto
  ): Promise<CreateApiKeyResultDto> {
    await this.assertApiAccessPlan(organizationId);

    const rawKey = this.buildRawKey();
    const keyHash = this.hashKey(rawKey);
    const keyPrefix = rawKey.substring(0, KEY_PREFIX_DISPLAY_LENGTH);

    const apiKey = ApiKey.create({
      id: uuidv4(),
      organizationId,
      userId,
      name: dto.name,
      keyHash,
      keyPrefix,
      expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
    });

    const saved = await this.apiKeyRepository.save(apiKey);

    this.logger.log(`API key created: ${saved.id} for org ${organizationId}`);

    return {
      id: saved.id,
      name: saved.name,
      keyPrefix: saved.keyPrefix,
      isActive: saved.isActive,
      lastUsedAt: saved.lastUsedAt,
      expiresAt: saved.expiresAt,
      createdAt: saved.createdAt,
      fullKey: rawKey,
    };
  }

  /**
   * List all API keys for an organisation. Never exposes key hashes.
   */
  async listApiKeys(organizationId: string): Promise<ApiKeyDto[]> {
    const keys = await this.apiKeyRepository.findByOrganizationId(organizationId);
    return keys.map(k => this.toDto(k));
  }

  /**
   * Revoke (deactivate) an API key.
   */
  async revokeApiKey(keyId: string, organizationId: string): Promise<void> {
    const key = await this.apiKeyRepository.findById(keyId);

    if (!key || key.organizationId !== organizationId) {
      throw new NotFoundException('Clé API introuvable');
    }

    const revoked = key.revoke();
    await this.apiKeyRepository.save(revoked);

    this.logger.log(`API key revoked: ${keyId} for org ${organizationId}`);
  }

  /**
   * Validate an inbound raw API key and return the user context.
   * Returns null if the key is invalid, expired, or the plan is insufficient.
   * Also asynchronously updates lastUsedAt.
   */
  async validateAndGetUser(rawKey: string): Promise<ApiKeyUserContext | null> {
    if (!rawKey?.startsWith('xped_live_')) return null;

    const keyHash = this.hashKey(rawKey);
    const apiKey = await this.apiKeyRepository.findByKeyHash(keyHash);

    if (!apiKey || !apiKey.isValid()) return null;

    // Real-time plan check — in case the org downgraded after key creation
    const subscription = await this.subscriptionRepository.findByOrganizationId(
      apiKey.organizationId
    );

    if (!subscription || !subscription.hasFeature('api_access')) {
      this.logger.warn(
        `API key used but org ${apiKey.organizationId} no longer has api_access feature`
      );
      return null;
    }

    // Update lastUsedAt asynchronously — don't block the request
    this.apiKeyRepository
      .save(apiKey.recordUsage())
      .catch(err => this.logger.warn(`Failed to update lastUsedAt for key ${apiKey.id}: ${err}`));

    const user = await this.userRepository.findById(apiKey.userId);
    if (!user || !user.isActive) return null;

    return {
      id: user.id,
      email: user.email,
      role: user.role,
      organizationId: user.organizationId,
      firstName: user.firstName,
      lastName: user.lastName,
      plan: subscription.plan.value,
      planFeatures: [...subscription.plan.planFeatures],
    };
  }

  // ── Helpers ─────────────────────────────────────────────────────────────

  private async assertApiAccessPlan(organizationId: string): Promise<void> {
    const subscription = await this.subscriptionRepository.findByOrganizationId(organizationId);

    if (!subscription || !subscription.hasFeature('api_access')) {
      throw new ForbiddenException(
        "L'accès API nécessite un abonnement Gold ou Platinium. Mettez à niveau votre abonnement pour générer des clés API."
      );
    }
  }

  /** Format: xped_live_<64 random hex chars> */
  private buildRawKey(): string {
    return `xped_live_${crypto.randomBytes(32).toString('hex')}`;
  }

  private hashKey(rawKey: string): string {
    return crypto.createHash('sha256').update(rawKey).digest('hex');
  }

  private toDto(apiKey: ApiKey): ApiKeyDto {
    return {
      id: apiKey.id,
      name: apiKey.name,
      keyPrefix: apiKey.keyPrefix,
      isActive: apiKey.isActive,
      lastUsedAt: apiKey.lastUsedAt,
      expiresAt: apiKey.expiresAt,
      createdAt: apiKey.createdAt,
    };
  }
}
