import { Repository, IsNull, LessThan } from 'typeorm';
import { randomBytes, createHash } from 'crypto';
import { ApiKey } from '../entities';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface CreateApiKeyDto {
  name: string;
  description?: string;
  scopes?: string[];
  expiresAt?: Date;
  ipWhitelist?: string[];
}

export interface UpdateApiKeyDto {
  name?: string;
  description?: string;
  scopes?: string[];
  expiresAt?: Date;
  ipWhitelist?: string[];
  isActive?: boolean;
}

export interface ApiKeyFilters {
  userId?: string;
  isActive?: boolean;
  isExpired?: boolean;
  search?: string;
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface ApiKeyWithSecret {
  apiKey: ApiKey;
  secret: string;
}

export class ApiKeyService {
  constructor(private readonly apiKeyRepository: Repository<ApiKey>) {}

  async create(ctx: ServiceContext, data: CreateApiKeyDto): Promise<ApiKeyWithSecret> {
    const secret = this.generateApiKey();
    const keyHash = this.hashKey(secret);
    const keyPrefix = secret.substring(0, 8);

    const apiKey = this.apiKeyRepository.create({
      tenantId: ctx.tenantId,
      userId: ctx.userId,
      name: data.name,
      description: data.description,
      keyHash,
      keyPrefix,
      scopes: data.scopes || [],
      expiresAt: data.expiresAt,
      ipWhitelist: data.ipWhitelist || [],
      isActive: true,
      createdBy: ctx.userId ?? null,
    });

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

    return {
      apiKey: savedKey,
      secret: secret, // Only returned once at creation
    };
  }

  async findById(ctx: ServiceContext, id: string): Promise<ApiKey | null> {
    return this.apiKeyRepository.findOne({
      where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['user'],
    });
  }

  async findByPrefix(ctx: ServiceContext, prefix: string): Promise<ApiKey | null> {
    return this.apiKeyRepository.findOne({
      where: { tenantId: ctx.tenantId, keyPrefix: prefix, deletedAt: IsNull() },
      relations: ['user'],
    });
  }

  async findByUserId(ctx: ServiceContext, userId: string): Promise<ApiKey[]> {
    return this.apiKeyRepository.find({
      where: { tenantId: ctx.tenantId, userId, deletedAt: IsNull() },
      order: { createdAt: 'DESC' },
    });
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: ApiKeyFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<ApiKey>> {
    const qb = this.apiKeyRepository
      .createQueryBuilder('apikey')
      .leftJoinAndSelect('apikey.user', 'user')
      .where('apikey.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('apikey.deleted_at IS NULL');

    if (filters.userId) {
      qb.andWhere('apikey.user_id = :userId', { userId: filters.userId });
    }

    if (filters.isActive !== undefined) {
      qb.andWhere('apikey.is_active = :isActive', { isActive: filters.isActive });
    }

    if (filters.isExpired !== undefined) {
      if (filters.isExpired) {
        qb.andWhere('apikey.expires_at < :now', { now: new Date() });
      } else {
        qb.andWhere('(apikey.expires_at IS NULL OR apikey.expires_at >= :now)', { now: new Date() });
      }
    }

    if (filters.search) {
      qb.andWhere('(apikey.name ILIKE :search OR apikey.description ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const [data, total] = await qb
      .orderBy('apikey.created_at', 'DESC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async update(ctx: ServiceContext, id: string, data: UpdateApiKeyDto): Promise<ApiKey | null> {
    const apiKey = await this.findById(ctx, id);
    if (!apiKey) return null;

    Object.assign(apiKey, {
      ...data,
      updatedBy: ctx.userId ?? null,
    });

    return this.apiKeyRepository.save(apiKey);
  }

  async revoke(ctx: ServiceContext, id: string): Promise<boolean> {
    const apiKey = await this.findById(ctx, id);
    if (!apiKey) return false;

    apiKey.isActive = false;
    apiKey.revokedAt = new Date();
    apiKey.updatedBy = ctx.userId ?? null;
    await this.apiKeyRepository.save(apiKey);

    return true;
  }

  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const apiKey = await this.findById(ctx, id);
    if (!apiKey) return false;

    apiKey.deletedAt = new Date();
    apiKey.deletedBy = ctx.userId ?? null;
    await this.apiKeyRepository.save(apiKey);

    return true;
  }

  async validateKey(ctx: ServiceContext, secret: string): Promise<ApiKey | null> {
    const prefix = secret.substring(0, 8);
    const apiKey = await this.findByPrefix(ctx, prefix);

    if (!apiKey) return null;
    if (!apiKey.isActive) return null;
    if (apiKey.expiresAt && apiKey.expiresAt < new Date()) return null;

    const hash = this.hashKey(secret);
    if (hash !== apiKey.keyHash) return null;

    // Update last used
    apiKey.lastUsedAt = new Date();
    await this.apiKeyRepository.save(apiKey);

    return apiKey;
  }

  async validateKeyWithIp(ctx: ServiceContext, secret: string, ipAddress: string): Promise<ApiKey | null> {
    const apiKey = await this.validateKey(ctx, secret);
    if (!apiKey) return null;

    if (apiKey.ipWhitelist && apiKey.ipWhitelist.length > 0) {
      if (!apiKey.ipWhitelist.includes(ipAddress)) {
        return null;
      }
    }

    return apiKey;
  }

  async hasScope(ctx: ServiceContext, secret: string, requiredScope: string): Promise<boolean> {
    const apiKey = await this.validateKey(ctx, secret);
    if (!apiKey) return false;

    if (!apiKey.scopes || apiKey.scopes.length === 0) return true; // No scopes = full access
    return apiKey.scopes.includes(requiredScope) || apiKey.scopes.includes('*');
  }

  async rotate(ctx: ServiceContext, id: string): Promise<ApiKeyWithSecret | null> {
    const oldKey = await this.findById(ctx, id);
    if (!oldKey) return null;

    // Revoke old key
    await this.revoke(ctx, id);

    // Create new key with same settings
    return this.create(ctx, {
      name: oldKey.name,
      description: oldKey.description,
      scopes: oldKey.scopes,
      expiresAt: oldKey.expiresAt,
      ipWhitelist: oldKey.ipWhitelist,
    });
  }

  async cleanupExpired(ctx: ServiceContext): Promise<number> {
    const result = await this.apiKeyRepository.update(
      {
        tenantId: ctx.tenantId,
        isActive: true,
        expiresAt: LessThan(new Date()),
      },
      {
        isActive: false,
        revokedAt: new Date(),
      },
    );

    return result.affected ?? 0;
  }

  private generateApiKey(): string {
    return `sk_${randomBytes(32).toString('hex')}`;
  }

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