import { Repository, In } from 'typeorm';
import { createHash } from 'crypto';
import { Flag, TenantOverride } from '../entities';
import {
  CreateFlagDto,
  UpdateFlagDto,
  CreateTenantOverrideDto,
  UpdateTenantOverrideDto,
  FlagEvaluationResult,
} from '../dto';

export class FeatureFlagsService {
  constructor(
    private readonly flagRepository: Repository<Flag>,
    private readonly overrideRepository: Repository<TenantOverride>
  ) {}

  // ============================================
  // FLAGS - CRUD
  // ============================================

  async findAllFlags(): Promise<Flag[]> {
    return this.flagRepository.find({
      where: { isActive: true },
      order: { code: 'ASC' },
    });
  }

  async findAllFlagsIncludingInactive(): Promise<Flag[]> {
    return this.flagRepository.find({
      order: { code: 'ASC' },
    });
  }

  async findFlagById(id: string): Promise<Flag | null> {
    return this.flagRepository.findOne({
      where: { id },
      relations: ['overrides'],
    });
  }

  async findFlagByCode(code: string): Promise<Flag | null> {
    return this.flagRepository.findOne({
      where: { code },
      relations: ['overrides'],
    });
  }

  async findFlagsByTags(tags: string[]): Promise<Flag[]> {
    return this.flagRepository
      .createQueryBuilder('flag')
      .where('flag.is_active = true')
      .andWhere('flag.tags && :tags', { tags })
      .orderBy('flag.code', 'ASC')
      .getMany();
  }

  async createFlag(data: CreateFlagDto, createdBy?: string): Promise<Flag> {
    const flag = this.flagRepository.create({
      ...data,
      createdBy,
    });
    return this.flagRepository.save(flag);
  }

  async updateFlag(
    id: string,
    data: UpdateFlagDto,
    updatedBy?: string
  ): Promise<Flag | null> {
    const flag = await this.flagRepository.findOne({ where: { id } });
    if (!flag) return null;

    Object.assign(flag, data, { updatedBy });
    return this.flagRepository.save(flag);
  }

  async deleteFlag(id: string): Promise<boolean> {
    const result = await this.flagRepository.delete(id);
    return (result.affected ?? 0) > 0;
  }

  async softDeleteFlag(id: string, updatedBy?: string): Promise<Flag | null> {
    return this.updateFlag(id, { isActive: false }, updatedBy);
  }

  async toggleFlag(id: string, enabled: boolean, updatedBy?: string): Promise<Flag | null> {
    return this.updateFlag(id, { enabled }, updatedBy);
  }

  // ============================================
  // TENANT OVERRIDES - CRUD
  // ============================================

  async findOverridesForFlag(flagId: string): Promise<TenantOverride[]> {
    return this.overrideRepository.find({
      where: { flagId },
      order: { createdAt: 'DESC' },
    });
  }

  async findOverridesForTenant(tenantId: string): Promise<TenantOverride[]> {
    return this.overrideRepository.find({
      where: { tenantId },
      relations: ['flag'],
      order: { createdAt: 'DESC' },
    });
  }

  async findOverride(flagId: string, tenantId: string): Promise<TenantOverride | null> {
    return this.overrideRepository.findOne({
      where: { flagId, tenantId },
      relations: ['flag'],
    });
  }

  async findOverrideById(id: string): Promise<TenantOverride | null> {
    return this.overrideRepository.findOne({
      where: { id },
      relations: ['flag'],
    });
  }

  async createOverride(
    data: CreateTenantOverrideDto,
    createdBy?: string
  ): Promise<TenantOverride> {
    const override = this.overrideRepository.create({
      ...data,
      createdBy,
    });
    return this.overrideRepository.save(override);
  }

  async updateOverride(
    id: string,
    data: UpdateTenantOverrideDto
  ): Promise<TenantOverride | null> {
    const override = await this.overrideRepository.findOne({ where: { id } });
    if (!override) return null;

    Object.assign(override, data);
    return this.overrideRepository.save(override);
  }

  async deleteOverride(id: string): Promise<boolean> {
    const result = await this.overrideRepository.delete(id);
    return (result.affected ?? 0) > 0;
  }

  async deleteOverrideByFlagAndTenant(flagId: string, tenantId: string): Promise<boolean> {
    const result = await this.overrideRepository.delete({ flagId, tenantId });
    return (result.affected ?? 0) > 0;
  }

  // ============================================
  // FLAG EVALUATION
  // ============================================

  /**
   * Evaluates a single flag for a tenant.
   * Priority: tenant override > global flag > rollout > default (false)
   */
  async evaluateFlag(flagCode: string, tenantId: string): Promise<FlagEvaluationResult> {
    // 1. Find the flag
    const flag = await this.flagRepository.findOne({
      where: { code: flagCode, isActive: true },
    });

    if (!flag) {
      return { code: flagCode, enabled: false, source: 'default' };
    }

    // 2. Check tenant override
    const override = await this.overrideRepository.findOne({
      where: { flagId: flag.id, tenantId },
    });

    if (override) {
      // Check if override is expired
      if (!override.expiresAt || override.expiresAt > new Date()) {
        return { code: flagCode, enabled: override.enabled, source: 'override' };
      }
    }

    // 3. Check global flag state
    if (!flag.enabled) {
      return { code: flagCode, enabled: false, source: 'global' };
    }

    // 4. Evaluate rollout percentage
    if (flag.rolloutPercentage >= 100) {
      return { code: flagCode, enabled: true, source: 'global' };
    }

    if (flag.rolloutPercentage <= 0) {
      return { code: flagCode, enabled: false, source: 'rollout' };
    }

    // 5. Deterministic hash-based rollout
    const bucket = this.calculateBucket(flagCode, tenantId);
    const enabled = bucket < flag.rolloutPercentage;

    return { code: flagCode, enabled, source: 'rollout' };
  }

  /**
   * Evaluates multiple flags for a tenant in a single call.
   * More efficient than calling evaluateFlag multiple times.
   */
  async evaluateFlags(
    flagCodes: string[],
    tenantId: string
  ): Promise<FlagEvaluationResult[]> {
    // Get all requested flags in one query
    const flags = await this.flagRepository.find({
      where: { code: In(flagCodes), isActive: true },
    });

    const flagMap = new Map(flags.map((f) => [f.code, f]));

    // Get all overrides for this tenant and these flags in one query
    const flagIds = flags.map((f) => f.id);
    const overrides = await this.overrideRepository.find({
      where: { flagId: In(flagIds), tenantId },
    });

    const overrideMap = new Map(overrides.map((o) => [o.flagId, o]));
    const now = new Date();

    // Evaluate each flag
    return flagCodes.map((code) => {
      const flag = flagMap.get(code);

      if (!flag) {
        return { code, enabled: false, source: 'default' as const };
      }

      const override = overrideMap.get(flag.id);

      if (override && (!override.expiresAt || override.expiresAt > now)) {
        return { code, enabled: override.enabled, source: 'override' as const };
      }

      if (!flag.enabled) {
        return { code, enabled: false, source: 'global' as const };
      }

      if (flag.rolloutPercentage >= 100) {
        return { code, enabled: true, source: 'global' as const };
      }

      if (flag.rolloutPercentage <= 0) {
        return { code, enabled: false, source: 'rollout' as const };
      }

      const bucket = this.calculateBucket(code, tenantId);
      const enabled = bucket < flag.rolloutPercentage;

      return { code, enabled, source: 'rollout' as const };
    });
  }

  /**
   * Quick boolean check for a single flag.
   */
  async isEnabled(flagCode: string, tenantId: string): Promise<boolean> {
    const result = await this.evaluateFlag(flagCode, tenantId);
    return result.enabled;
  }

  // ============================================
  // MAINTENANCE
  // ============================================

  /**
   * Removes expired overrides from the database.
   * Should be called periodically via cron job.
   */
  async cleanupExpiredOverrides(): Promise<number> {
    const now = new Date();
    const result = await this.overrideRepository
      .createQueryBuilder()
      .delete()
      .where('expires_at IS NOT NULL')
      .andWhere('expires_at < :now', { now })
      .execute();

    return result.affected ?? 0;
  }

  /**
   * Gets statistics for a flag including override counts.
   */
  async getFlagStats(flagId: string): Promise<{
    flag: Flag | null;
    overrideCount: number;
    enabledOverrides: number;
    disabledOverrides: number;
  }> {
    const flag = await this.flagRepository.findOne({ where: { id: flagId } });

    if (!flag) {
      return {
        flag: null,
        overrideCount: 0,
        enabledOverrides: 0,
        disabledOverrides: 0,
      };
    }

    const now = new Date();
    const overrides = await this.overrideRepository
      .createQueryBuilder('o')
      .where('o.flag_id = :flagId', { flagId })
      .andWhere('(o.expires_at IS NULL OR o.expires_at > :now)', { now })
      .getMany();

    const enabledOverrides = overrides.filter((o) => o.enabled).length;
    const disabledOverrides = overrides.filter((o) => !o.enabled).length;

    return {
      flag,
      overrideCount: overrides.length,
      enabledOverrides,
      disabledOverrides,
    };
  }

  // ============================================
  // PRIVATE HELPERS
  // ============================================

  /**
   * Calculates a deterministic bucket (0-99) for rollout evaluation.
   * Uses MD5 hash of flag code + tenant ID for consistent results.
   */
  private calculateBucket(flagCode: string, tenantId: string): number {
    const input = `${flagCode}:${tenantId}`;
    const hash = createHash('md5').update(input).digest('hex');
    // Take first 8 chars of hash and convert to number
    const num = parseInt(hash.substring(0, 8), 16);
    return Math.abs(num % 100);
  }
}
