import crypto from 'crypto';
import { query, queryOne } from '../../config/database.js';
import { ValidationError, NotFoundError, UnauthorizedError } from '../../shared/types/index.js';
import { logger } from '../../shared/utils/logger.js';

// ============================================================================
// TYPES
// ============================================================================

export interface ApiKey {
  id: string;
  user_id: string;
  tenant_id: string;
  name: string;
  key_index: string;
  key_hash: string;
  scope: string | null;
  allowed_ips: string[] | null;
  expiration_date: Date | null;
  last_used_at: Date | null;
  is_active: boolean;
  created_at: Date;
  updated_at: Date;
}

export interface CreateApiKeyDto {
  user_id: string;
  tenant_id: string;
  name: string;
  scope?: string;
  allowed_ips?: string[];
  expiration_days?: number;
}

export interface UpdateApiKeyDto {
  name?: string;
  scope?: string;
  allowed_ips?: string[];
  expiration_date?: Date | null;
  is_active?: boolean;
}

export interface ApiKeyWithPlainKey {
  apiKey: Omit<ApiKey, 'key_hash'>;
  plainKey: string;
}

export interface ApiKeyValidationResult {
  valid: boolean;
  apiKey?: ApiKey;
  user?: {
    id: string;
    tenant_id: string;
    email: string;
    roles: string[];
  };
  error?: string;
}

export interface ApiKeyFilters {
  user_id?: string;
  tenant_id?: string;
  is_active?: boolean;
  scope?: string;
}

// ============================================================================
// CONSTANTS
// ============================================================================

const API_KEY_PREFIX = 'mgn_';
const KEY_LENGTH = 32; // 32 bytes = 256 bits
const HASH_ITERATIONS = 100000;
const HASH_KEYLEN = 64;
const HASH_DIGEST = 'sha512';

// ============================================================================
// SERVICE
// ============================================================================

class ApiKeysService {
  /**
   * Generate a cryptographically secure API key
   */
  private generatePlainKey(): string {
    const randomBytes = crypto.randomBytes(KEY_LENGTH);
    const key = randomBytes.toString('base64url');
    return `${API_KEY_PREFIX}${key}`;
  }

  /**
   * Extract the key index (first 16 chars after prefix) for lookup
   */
  private getKeyIndex(plainKey: string): string {
    const keyWithoutPrefix = plainKey.replace(API_KEY_PREFIX, '');
    return keyWithoutPrefix.substring(0, 16);
  }

  /**
   * Hash the API key using PBKDF2
   */
  private async hashKey(plainKey: string): Promise<string> {
    const salt = crypto.randomBytes(16).toString('hex');

    return new Promise((resolve, reject) => {
      crypto.pbkdf2(
        plainKey,
        salt,
        HASH_ITERATIONS,
        HASH_KEYLEN,
        HASH_DIGEST,
        (err, derivedKey) => {
          if (err) reject(err);
          resolve(`${salt}:${derivedKey.toString('hex')}`);
        }
      );
    });
  }

  /**
   * Verify a plain key against a stored hash
   */
  private async verifyKey(plainKey: string, storedHash: string): Promise<boolean> {
    const [salt, hash] = storedHash.split(':');

    return new Promise((resolve, reject) => {
      crypto.pbkdf2(
        plainKey,
        salt,
        HASH_ITERATIONS,
        HASH_KEYLEN,
        HASH_DIGEST,
        (err, derivedKey) => {
          if (err) reject(err);
          resolve(derivedKey.toString('hex') === hash);
        }
      );
    });
  }

  /**
   * Create a new API key
   * Returns the plain key only once - it cannot be retrieved later
   */
  async create(dto: CreateApiKeyDto): Promise<ApiKeyWithPlainKey> {
    // Validate user exists
    const user = await queryOne<{ id: string }>(
      'SELECT id FROM auth.users WHERE id = $1 AND tenant_id = $2',
      [dto.user_id, dto.tenant_id]
    );

    if (!user) {
      throw new ValidationError('Usuario no encontrado');
    }

    // Check for duplicate name
    const existing = await queryOne<{ id: string }>(
      'SELECT id FROM auth.api_keys WHERE user_id = $1 AND name = $2',
      [dto.user_id, dto.name]
    );

    if (existing) {
      throw new ValidationError('Ya existe una API key con ese nombre');
    }

    // Generate key
    const plainKey = this.generatePlainKey();
    const keyIndex = this.getKeyIndex(plainKey);
    const keyHash = await this.hashKey(plainKey);

    // Calculate expiration date
    let expirationDate: Date | null = null;
    if (dto.expiration_days) {
      expirationDate = new Date();
      expirationDate.setDate(expirationDate.getDate() + dto.expiration_days);
    }

    // Insert API key
    const apiKey = await queryOne<ApiKey>(
      `INSERT INTO auth.api_keys (
        user_id, tenant_id, name, key_index, key_hash,
        scope, allowed_ips, expiration_date, is_active
      ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
      RETURNING id, user_id, tenant_id, name, key_index, scope,
                allowed_ips, expiration_date, is_active, created_at, updated_at`,
      [
        dto.user_id,
        dto.tenant_id,
        dto.name,
        keyIndex,
        keyHash,
        dto.scope || null,
        dto.allowed_ips || null,
        expirationDate,
      ]
    );

    if (!apiKey) {
      throw new Error('Error al crear API key');
    }

    logger.info('API key created', {
      apiKeyId: apiKey.id,
      userId: dto.user_id,
      name: dto.name
    });

    return {
      apiKey,
      plainKey, // Only returned once!
    };
  }

  /**
   * Find all API keys for a user/tenant
   */
  async findAll(filters: ApiKeyFilters): Promise<Omit<ApiKey, 'key_hash'>[]> {
    const conditions: string[] = [];
    const params: any[] = [];
    let paramIndex = 1;

    if (filters.user_id) {
      conditions.push(`user_id = $${paramIndex++}`);
      params.push(filters.user_id);
    }

    if (filters.tenant_id) {
      conditions.push(`tenant_id = $${paramIndex++}`);
      params.push(filters.tenant_id);
    }

    if (filters.is_active !== undefined) {
      conditions.push(`is_active = $${paramIndex++}`);
      params.push(filters.is_active);
    }

    if (filters.scope) {
      conditions.push(`scope = $${paramIndex++}`);
      params.push(filters.scope);
    }

    const whereClause = conditions.length > 0
      ? `WHERE ${conditions.join(' AND ')}`
      : '';

    const apiKeys = await query<ApiKey>(
      `SELECT id, user_id, tenant_id, name, key_index, scope,
              allowed_ips, expiration_date, last_used_at, is_active,
              created_at, updated_at
       FROM auth.api_keys
       ${whereClause}
       ORDER BY created_at DESC`,
      params
    );

    return apiKeys;
  }

  /**
   * Find a specific API key by ID
   */
  async findById(id: string, tenantId: string): Promise<Omit<ApiKey, 'key_hash'> | null> {
    const apiKey = await queryOne<ApiKey>(
      `SELECT id, user_id, tenant_id, name, key_index, scope,
              allowed_ips, expiration_date, last_used_at, is_active,
              created_at, updated_at
       FROM auth.api_keys
       WHERE id = $1 AND tenant_id = $2`,
      [id, tenantId]
    );

    return apiKey;
  }

  /**
   * Update an API key
   */
  async update(id: string, tenantId: string, dto: UpdateApiKeyDto): Promise<Omit<ApiKey, 'key_hash'>> {
    const existing = await this.findById(id, tenantId);
    if (!existing) {
      throw new NotFoundError('API key no encontrada');
    }

    const updates: string[] = ['updated_at = NOW()'];
    const params: any[] = [];
    let paramIndex = 1;

    if (dto.name !== undefined) {
      updates.push(`name = $${paramIndex++}`);
      params.push(dto.name);
    }

    if (dto.scope !== undefined) {
      updates.push(`scope = $${paramIndex++}`);
      params.push(dto.scope);
    }

    if (dto.allowed_ips !== undefined) {
      updates.push(`allowed_ips = $${paramIndex++}`);
      params.push(dto.allowed_ips);
    }

    if (dto.expiration_date !== undefined) {
      updates.push(`expiration_date = $${paramIndex++}`);
      params.push(dto.expiration_date);
    }

    if (dto.is_active !== undefined) {
      updates.push(`is_active = $${paramIndex++}`);
      params.push(dto.is_active);
    }

    params.push(id);
    params.push(tenantId);

    const updated = await queryOne<ApiKey>(
      `UPDATE auth.api_keys
       SET ${updates.join(', ')}
       WHERE id = $${paramIndex++} AND tenant_id = $${paramIndex}
       RETURNING id, user_id, tenant_id, name, key_index, scope,
                 allowed_ips, expiration_date, last_used_at, is_active,
                 created_at, updated_at`,
      params
    );

    if (!updated) {
      throw new Error('Error al actualizar API key');
    }

    logger.info('API key updated', { apiKeyId: id });

    return updated;
  }

  /**
   * Revoke (soft delete) an API key
   */
  async revoke(id: string, tenantId: string): Promise<void> {
    const result = await query(
      `UPDATE auth.api_keys
       SET is_active = false, updated_at = NOW()
       WHERE id = $1 AND tenant_id = $2`,
      [id, tenantId]
    );

    if (!result) {
      throw new NotFoundError('API key no encontrada');
    }

    logger.info('API key revoked', { apiKeyId: id });
  }

  /**
   * Delete an API key permanently
   */
  async delete(id: string, tenantId: string): Promise<void> {
    const result = await query(
      'DELETE FROM auth.api_keys WHERE id = $1 AND tenant_id = $2',
      [id, tenantId]
    );

    logger.info('API key deleted', { apiKeyId: id });
  }

  /**
   * Validate an API key and return the associated user info
   * This is the main method used by the authentication middleware
   */
  async validate(plainKey: string, clientIp?: string): Promise<ApiKeyValidationResult> {
    // Check prefix
    if (!plainKey.startsWith(API_KEY_PREFIX)) {
      return { valid: false, error: 'Formato de API key inválido' };
    }

    // Extract key index for lookup
    const keyIndex = this.getKeyIndex(plainKey);

    // Find API key by index
    const apiKey = await queryOne<ApiKey>(
      `SELECT * FROM auth.api_keys
       WHERE key_index = $1 AND is_active = true`,
      [keyIndex]
    );

    if (!apiKey) {
      return { valid: false, error: 'API key no encontrada o inactiva' };
    }

    // Verify hash
    const isValid = await this.verifyKey(plainKey, apiKey.key_hash);
    if (!isValid) {
      return { valid: false, error: 'API key inválida' };
    }

    // Check expiration
    if (apiKey.expiration_date && new Date(apiKey.expiration_date) < new Date()) {
      return { valid: false, error: 'API key expirada' };
    }

    // Check IP whitelist
    if (apiKey.allowed_ips && apiKey.allowed_ips.length > 0 && clientIp) {
      if (!apiKey.allowed_ips.includes(clientIp)) {
        logger.warn('API key IP not allowed', {
          apiKeyId: apiKey.id,
          clientIp,
          allowedIps: apiKey.allowed_ips
        });
        return { valid: false, error: 'IP no autorizada' };
      }
    }

    // Get user info with roles
    const user = await queryOne<{
      id: string;
      tenant_id: string;
      email: string;
      role_codes: string[];
    }>(
      `SELECT u.id, u.tenant_id, u.email, array_agg(r.code) as role_codes
       FROM auth.users u
       LEFT JOIN auth.user_roles ur ON u.id = ur.user_id
       LEFT JOIN auth.roles r ON ur.role_id = r.id
       WHERE u.id = $1 AND u.status = 'active'
       GROUP BY u.id`,
      [apiKey.user_id]
    );

    if (!user) {
      return { valid: false, error: 'Usuario asociado no encontrado o inactivo' };
    }

    // Update last used timestamp (async, don't wait)
    query(
      'UPDATE auth.api_keys SET last_used_at = NOW() WHERE id = $1',
      [apiKey.id]
    ).catch(err => logger.error('Error updating last_used_at', { error: err }));

    return {
      valid: true,
      apiKey,
      user: {
        id: user.id,
        tenant_id: user.tenant_id,
        email: user.email,
        roles: user.role_codes?.filter(Boolean) || [],
      },
    };
  }

  /**
   * Regenerate an API key (creates new key, invalidates old)
   */
  async regenerate(id: string, tenantId: string): Promise<ApiKeyWithPlainKey> {
    const existing = await queryOne<ApiKey>(
      'SELECT * FROM auth.api_keys WHERE id = $1 AND tenant_id = $2',
      [id, tenantId]
    );

    if (!existing) {
      throw new NotFoundError('API key no encontrada');
    }

    // Generate new key
    const plainKey = this.generatePlainKey();
    const keyIndex = this.getKeyIndex(plainKey);
    const keyHash = await this.hashKey(plainKey);

    // Update with new key
    const updated = await queryOne<ApiKey>(
      `UPDATE auth.api_keys
       SET key_index = $1, key_hash = $2, updated_at = NOW()
       WHERE id = $3 AND tenant_id = $4
       RETURNING id, user_id, tenant_id, name, key_index, scope,
                 allowed_ips, expiration_date, is_active, created_at, updated_at`,
      [keyIndex, keyHash, id, tenantId]
    );

    if (!updated) {
      throw new Error('Error al regenerar API key');
    }

    logger.info('API key regenerated', { apiKeyId: id });

    return {
      apiKey: updated,
      plainKey,
    };
  }
}

export const apiKeysService = new ApiKeysService();
