import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { ApiKey } from '../../entities/api-key.entity';
import { License } from '../../entities/license.entity';

/** Shape returned to the caller on creation — the ONLY time the plaintext key is exposed. */
export interface CreatedApiKey {
  /** Full plaintext key — show once, store nowhere. */
  plaintext_key: string;
  id: string;
  name: string;
  key_prefix: string;
  is_active: boolean;
  created_at: Date;
}

/** Safe list view — no hash, no plaintext. */
export interface ApiKeyListItem {
  id: string;
  name: string;
  key_prefix: string;
  last_used_at: Date | null;
  is_active: boolean;
  created_at: Date;
}

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

  constructor(
    @InjectRepository(ApiKey)
    private readonly apiKeyRepo: Repository<ApiKey>,
    @InjectRepository(License)
    private readonly licenseRepo: Repository<License>,
  ) {}

  /**
   * Issue a new API key for the given license.
   *
   * Key format: `corr_<prefix8>_<secret32>`
   * where prefix and secret are URL-safe base64url random bytes.
   *
   * Returns the full plaintext key ONCE alongside the saved row.
   * The hash is never returned to the caller.
   */
  async create(licenseId: string, name: string): Promise<CreatedApiKey> {
    const prefixBytes = crypto.randomBytes(6); // 8 base64url chars
    const secretBytes = crypto.randomBytes(24); // 32 base64url chars

    const prefix = prefixBytes.toString('base64url');
    const secret = secretBytes.toString('base64url');
    const plaintextKey = `corr_${prefix}_${secret}`;

    const keyHash = crypto
      .createHash('sha256')
      .update(plaintextKey)
      .digest('hex');

    const entity = this.apiKeyRepo.create({
      license_id: licenseId,
      name,
      key_prefix: prefix,
      key_hash: keyHash,
      is_active: true,
    });

    const saved = await this.apiKeyRepo.save(entity);

    this.logger.log(
      `API key created: id=${saved.id} prefix=${prefix} license=${licenseId}`,
    );

    return {
      plaintext_key: plaintextKey,
      id: saved.id,
      name: saved.name,
      key_prefix: saved.key_prefix,
      is_active: saved.is_active,
      created_at: saved.created_at,
    };
  }

  /**
   * List all keys (active and revoked) for a license.
   * The key_hash is intentionally excluded.
   */
  async list(licenseId: string): Promise<ApiKeyListItem[]> {
    const rows = await this.apiKeyRepo.find({
      where: { license_id: licenseId },
      order: { created_at: 'DESC' },
      select: ['id', 'name', 'key_prefix', 'last_used_at', 'is_active', 'created_at'],
    });

    return rows.map((r) => ({
      id: r.id,
      name: r.name,
      key_prefix: r.key_prefix,
      last_used_at: r.last_used_at,
      is_active: r.is_active,
      created_at: r.created_at,
    }));
  }

  /**
   * Revoke (soft-delete) a key.
   * Returns the updated row or throws NotFoundException if the key
   * doesn't exist within this license.
   */
  async revoke(licenseId: string, id: string): Promise<{ id: string; is_active: boolean }> {
    const key = await this.apiKeyRepo.findOne({
      where: { id, license_id: licenseId },
    });

    if (!key) {
      throw new NotFoundException(`API key ${id} not found`);
    }

    key.is_active = false;
    await this.apiKeyRepo.save(key);

    this.logger.log(`API key revoked: id=${id} license=${licenseId}`);

    return { id: key.id, is_active: key.is_active };
  }

  /**
   * Validate a raw API key string. Called by JwtAuthGuard.
   *
   * Hashes the raw key, looks up an ACTIVE row, touches last_used_at, resolves
   * the license owner (so the guard can attribute the call to a real user UUID),
   * and returns { license_id, user_id } on success or null on failure.
   *
   * user_id is the license owner — API-key calls act AS the owner, so any
   * created_by / @CurrentUser FK insert gets a valid UUID and correct attribution.
   */
  async validateKey(
    rawKey: string,
  ): Promise<{ license_id: string; user_id: string | null } | null> {
    const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');

    const key = await this.apiKeyRepo.findOne({
      where: { key_hash: keyHash, is_active: true },
      select: ['id', 'license_id'],
    });

    if (!key) {
      return null;
    }

    // Update last_used_at without loading the full row again.
    await this.apiKeyRepo.update(key.id, { last_used_at: new Date() });

    const license = await this.licenseRepo.findOne({
      where: { id: key.license_id },
      select: ['id', 'owner_user_id'],
    });

    return { license_id: key.license_id, user_id: license?.owner_user_id ?? null };
  }
}
