import { BaseService } from '../../../core/interfaces/IService';
import { eventBus } from '../../../core/events';
import { IPayrollCalculator } from './interfaces/IPayrollCalculator';
import { IRewardService } from './interfaces/IRewardService';
import {
  PayrollBatch,
  PayrollEntry,
  PayrollCalculationInput,
  PayrollCalculationResult,
  PayrollSummary,
  PayrollFilter,
  PayrollConfig,
  PayrollStatus,
  PayrollPeriod,
  defaultPayrollConfig,
  PayrollRewardBreakdown
} from '../types/payroll.types';
import { RewardType, RewardStatus } from '../types/reward.types';

/**
 * Payroll calculator service implementation
 */
export class PayrollCalculator extends BaseService implements IPayrollCalculator {
  readonly id = 'payroll-calculator';
  readonly name = 'Payroll Calculation Service';
  
  private batches: Map<string, PayrollBatch> = new Map();
  private entries: Map<string, PayrollEntry> = new Map();
  private config: PayrollConfig = { ...defaultPayrollConfig };

  constructor(private rewardServiceFactory: () => IRewardService) {
    super();
  }

  async initialize(): Promise<void> {
    await super.initialize();
    await this.loadBatchesFromStorage();
    await this.loadConfigFromStorage();
    console.log('PayrollCalculator initialized');
  }

  async calculatePayroll(input: PayrollCalculationInput): Promise<PayrollCalculationResult> {
    // Validate input
    const validation = await this.validatePayrollCalculation(input);
    if (!validation.valid) {
      throw new Error(`Payroll calculation validation failed: ${validation.errors.join(', ')}`);
    }

    // Get all approved rewards for the period
    const rewards = await this.rewardServiceFactory().getTotalRewardsByPeriod(
      input.startDate,
      input.endDate,
      RewardStatus.APPROVED
    );

    // Get user rewards grouped by user
    const userRewards = new Map<string, any[]>();
    
    // In a real implementation, we would fetch actual reward data
    // For now, we'll simulate some data
    const simulatedUsers = input.userIds || ['user1', 'user2', 'user3'];
    
    const entries: PayrollEntry[] = [];
    let totalAmount = 0;

    for (const userId of simulatedUsers) {
      const userRewardData = await this.rewardServiceFactory().getRewardsByUserId(userId, {
        status: RewardStatus.APPROVED,
        dateFrom: input.startDate,
        dateTo: input.endDate
      });

      if (userRewardData.length === 0) continue;

      // Calculate reward breakdown for this user
      const rewardBreakdown = this.calculateRewardBreakdown(userRewardData);
      const userTotalAmount = rewardBreakdown.reduce((sum, rb) => sum + rb.totalAmount, 0);

      if (userTotalAmount < this.config.minimumAmount) continue;

      const entryId = this.generateId('entry');
      const entry: PayrollEntry = {
        id: entryId,
        userId,
        payrollId: '', // Will be set when batch is created
        rewardBreakdown,
        totalAmount: userTotalAmount,
        currency: this.config.currency,
        status: PayrollStatus.DRAFT,
        calculatedAt: new Date()
      };

      entries.push(entry);
      totalAmount += userTotalAmount;
    }

    // Create batch
    const batchId = this.generateId('batch');
    const batch: PayrollBatch = {
      id: batchId,
      period: input.period,
      startDate: input.startDate,
      endDate: input.endDate,
      status: PayrollStatus.CALCULATING,
      totalParticipants: entries.length,
      totalAmount,
      currency: this.config.currency,
      entries: entries.map(e => ({ ...e, payrollId: batchId })),
      createdAt: new Date(),
      updatedAt: new Date(),
      createdBy: 'system' // In real implementation, this would be the current user
    };

    // Update entries with payroll ID
    entries.forEach(entry => {
      entry.payrollId = batchId;
      this.entries.set(entry.id, entry);
    });

    this.batches.set(batchId, batch);

    // Calculate summary
    const summary = this.calculatePayrollSummary(entries);

    // Update batch status
    await this.updatePayrollBatchStatus(batchId, PayrollStatus.PENDING_APPROVAL, 'system');

    // Emit event
    await eventBus.emit('payroll.calculated', {
      batchId,
      period: input.period,
      totalAmount,
      participantCount: entries.length,
      calculatedAt: new Date()
    });

    return {
      batchId,
      totalParticipants: entries.length,
      totalAmount,
      currency: this.config.currency,
      entries,
      summary
    };
  }

  async createPayrollBatch(batch: Omit<PayrollBatch, 'id' | 'createdAt' | 'updatedAt'>): Promise<PayrollBatch> {
    const id = this.generateId('batch');
    const now = new Date();

    const newBatch: PayrollBatch = {
      ...batch,
      id,
      createdAt: now,
      updatedAt: now
    };

    this.batches.set(id, newBatch);

    // Store entries
    batch.entries.forEach(entry => {
      this.entries.set(entry.id, { ...entry, payrollId: id });
    });

    return newBatch;
  }

  async getPayrollBatchById(batchId: string): Promise<PayrollBatch | null> {
    return this.batches.get(batchId) || null;
  }

  async getPayrollBatches(filter?: PayrollFilter): Promise<PayrollBatch[]> {
    let batches = Array.from(this.batches.values());

    if (filter) {
      if (filter.period) {
        batches = batches.filter(b => b.period === filter.period);
      }
      if (filter.status) {
        batches = batches.filter(b => b.status === filter.status);
      }
      if (filter.startDate) {
        batches = batches.filter(b => b.startDate >= filter.startDate!);
      }
      if (filter.endDate) {
        batches = batches.filter(b => b.endDate <= filter.endDate!);
      }
      if (filter.createdBy) {
        batches = batches.filter(b => b.createdBy === filter.createdBy);
      }
    }

    // Sort by creation date (newest first)
    batches.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());

    // Apply pagination
    if (filter?.offset) {
      batches = batches.slice(filter.offset);
    }
    if (filter?.limit) {
      batches = batches.slice(0, filter.limit);
    }

    return batches;
  }

  async getPayrollEntryById(entryId: string): Promise<PayrollEntry | null> {
    return this.entries.get(entryId) || null;
  }

  async getPayrollEntriesByUserId(userId: string, filter?: PayrollFilter): Promise<PayrollEntry[]> {
    let entries = Array.from(this.entries.values()).filter(e => e.userId === userId);

    if (filter) {
      if (filter.status) {
        entries = entries.filter(e => e.status === filter.status);
      }
      // Add more filters as needed
    }

    // Sort by calculation date (newest first)
    entries.sort((a, b) => b.calculatedAt.getTime() - a.calculatedAt.getTime());

    return entries;
  }

  async updatePayrollBatchStatus(batchId: string, status: PayrollStatus, updatedBy: string): Promise<PayrollBatch> {
    const batch = this.batches.get(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    const now = new Date();
    const updatedBatch = {
      ...batch,
      status,
      updatedAt: now,
      ...(status === PayrollStatus.APPROVED ? { approvedAt: now } : {}),
      ...(status === PayrollStatus.COMPLETED ? { completedAt: now } : {})
    };

    this.batches.set(batchId, updatedBatch);

    // Update all entries in the batch
    batch.entries.forEach(entry => {
      const updatedEntry = { ...entry, status, updatedAt: now };
      this.entries.set(entry.id, updatedEntry);
    });

    // Emit event
    await eventBus.emit('payroll.status.updated', {
      batchId,
      oldStatus: batch.status,
      newStatus: status,
      updatedBy,
      updatedAt: now
    });

    return updatedBatch;
  }

  async approvePayrollBatch(batchId: string, approvedBy: string): Promise<PayrollBatch> {
    const batch = await this.updatePayrollBatchStatus(batchId, PayrollStatus.APPROVED, approvedBy);
    
    // Update approved by field
    const updatedBatch = {
      ...batch,
      approvedBy,
      approvedAt: new Date()
    };
    
    this.batches.set(batchId, updatedBatch);
    return updatedBatch;
  }

  async processPayrollBatch(batchId: string): Promise<PayrollBatch> {
    const batch = this.batches.get(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    if (batch.status !== PayrollStatus.APPROVED) {
      throw new Error(`Payroll batch must be approved before processing: ${batchId}`);
    }

    // Mark rewards as paid
    for (const entry of batch.entries) {
      for (const breakdown of entry.rewardBreakdown) {
        for (const rewardId of breakdown.rewardIds) {
          await this.rewardServiceFactory().markRewardAsPaid(rewardId, `payroll_${batchId}`);
        }
      }
    }

    return this.updatePayrollBatchStatus(batchId, PayrollStatus.COMPLETED, 'system');
  }

  async cancelPayrollBatch(batchId: string, reason: string, cancelledBy: string): Promise<PayrollBatch> {
    const batch = this.batches.get(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    const updatedBatch = {
      ...batch,
      status: PayrollStatus.FAILED,
      updatedAt: new Date(),
      notes: reason
    };

    this.batches.set(batchId, updatedBatch);

    // Emit event
    await eventBus.emit('payroll.cancelled', {
      batchId,
      reason,
      cancelledBy,
      cancelledAt: updatedBatch.updatedAt
    });

    return updatedBatch;
  }

  async getPayrollSummary(startDate: Date, endDate: Date): Promise<PayrollSummary> {
    const batches = await this.getPayrollBatches({
      startDate,
      endDate,
      status: PayrollStatus.COMPLETED
    });

    const allEntries = batches.flatMap(b => b.entries);
    return this.calculatePayrollSummary(allEntries);
  }

  async getUserPayrollHistory(userId: string, limit?: number, offset?: number) {
    let entries = await this.getPayrollEntriesByUserId(userId);
    
    const total = entries.length;
    const totalEarned = entries
      .filter(e => e.status === PayrollStatus.COMPLETED)
      .reduce((sum, e) => sum + e.totalAmount, 0);

    if (offset) {
      entries = entries.slice(offset);
    }
    if (limit) {
      entries = entries.slice(0, limit);
    }

    return {
      entries,
      total,
      totalEarned
    };
  }

  async recalculatePayrollBatch(batchId: string): Promise<PayrollBatch> {
    const batch = this.batches.get(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    if (batch.status === PayrollStatus.COMPLETED) {
      throw new Error(`Cannot recalculate completed payroll batch: ${batchId}`);
    }

    // Recalculate using original parameters
    const input: PayrollCalculationInput = {
      period: batch.period,
      startDate: batch.startDate,
      endDate: batch.endDate,
      userIds: batch.entries.map(e => e.userId)
    };

    const result = await this.calculatePayroll(input);
    
    // Update existing batch
    const updatedBatch = {
      ...batch,
      entries: result.entries,
      totalAmount: result.totalAmount,
      totalParticipants: result.totalParticipants,
      updatedAt: new Date(),
      calculatedAt: new Date()
    };

    this.batches.set(batchId, updatedBatch);
    return updatedBatch;
  }

  async getPayrollConfig(): Promise<PayrollConfig> {
    return { ...this.config };
  }

  async updatePayrollConfig(config: Partial<PayrollConfig>): Promise<PayrollConfig> {
    this.config = { ...this.config, ...config };
    await this.saveConfigToStorage();
    return { ...this.config };
  }

  async generatePayrollReport(batchId: string) {
    const batch = await this.getPayrollBatchById(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    const summary = this.calculatePayrollSummary(batch.entries);

    return {
      batchInfo: batch,
      summary,
      entries: batch.entries,
      exportUrl: `/api/payroll/export/${batchId}` // In real implementation
    };
  }

  async validatePayrollCalculation(input: PayrollCalculationInput) {
    const errors: string[] = [];
    const warnings: string[] = [];

    if (input.startDate >= input.endDate) {
      errors.push('Start date must be before end date');
    }

    if (input.endDate > new Date()) {
      warnings.push('End date is in the future');
    }

    const daysDiff = (input.endDate.getTime() - input.startDate.getTime()) / (1000 * 60 * 60 * 24);
    
    switch (input.period) {
      case PayrollPeriod.WEEKLY:
        if (daysDiff > 7) {
          warnings.push('Period is longer than typical weekly period');
        }
        break;
      case PayrollPeriod.MONTHLY:
        if (daysDiff > 31) {
          warnings.push('Period is longer than typical monthly period');
        }
        break;
    }

    return {
      valid: errors.length === 0,
      errors,
      warnings
    };
  }

  async getPendingPayrollBatches(): Promise<PayrollBatch[]> {
    return this.getPayrollBatches({
      status: PayrollStatus.PENDING_APPROVAL
    });
  }

  async autoCalculateScheduledPayrolls(): Promise<PayrollBatch[]> {
    // In a real implementation, this would check for scheduled payroll calculations
    // based on configuration and create new batches automatically
    return [];
  }

  async getPayrollStatistics(period: PayrollPeriod, count: number) {
    const batches = await this.getPayrollBatches({
      period,
      status: PayrollStatus.COMPLETED,
      limit: count
    });

    const totalBatches = batches.length;
    const totalAmount = batches.reduce((sum, b) => sum + b.totalAmount, 0);
    const totalParticipants = batches.reduce((sum, b) => sum + b.totalParticipants, 0);
    const averageAmount = totalBatches > 0 ? totalAmount / totalBatches : 0;

    const periodBreakdown = batches.map(batch => ({
      period: `${batch.startDate.toISOString().substring(0, 7)}`, // YYYY-MM
      amount: batch.totalAmount,
      participants: batch.totalParticipants,
      batches: 1
    }));

    return {
      totalBatches,
      totalAmount,
      totalParticipants,
      averageAmount,
      periodBreakdown
    };
  }

  async exportPayrollData(batchId: string, format: 'csv' | 'excel' | 'pdf') {
    const batch = await this.getPayrollBatchById(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    // In a real implementation, this would generate the actual file
    const filename = `payroll_${batchId}_${Date.now()}.${format}`;
    
    return {
      filename,
      url: `/api/payroll/download/${filename}`,
      size: 1024 // Mock size
    };
  }

  async importPayrollAdjustments(batchId: string, adjustments: Array<{ userId: string; amount: number; reason: string }>) {
    const batch = this.batches.get(batchId);
    if (!batch) {
      throw new Error(`Payroll batch not found: ${batchId}`);
    }

    if (batch.status === PayrollStatus.COMPLETED) {
      throw new Error(`Cannot adjust completed payroll batch: ${batchId}`);
    }

    // Apply adjustments to entries
    for (const adjustment of adjustments) {
      const entry = batch.entries.find(e => e.userId === adjustment.userId);
      if (entry) {
        entry.totalAmount += adjustment.amount;
        entry.notes = adjustment.reason;
        this.entries.set(entry.id, entry);
      }
    }

    // Recalculate batch totals
    const updatedBatch = {
      ...batch,
      totalAmount: batch.entries.reduce((sum, e) => sum + e.totalAmount, 0),
      updatedAt: new Date()
    };

    this.batches.set(batchId, updatedBatch);
    return updatedBatch;
  }

  async getPayrollAuditTrail(batchId: string) {
    // In a real implementation, this would fetch audit logs from storage
    return [
      {
        action: 'created',
        performedBy: 'system',
        performedAt: new Date(),
        details: { batchId }
      }
    ];
  }

  async calculateNextPayrollDate(period: PayrollPeriod, lastPayrollDate?: Date): Promise<Date> {
    const baseDate = lastPayrollDate || new Date();
    const nextDate = new Date(baseDate);

    switch (period) {
      case PayrollPeriod.WEEKLY:
        nextDate.setDate(nextDate.getDate() + 7);
        break;
      case PayrollPeriod.BIWEEKLY:
        nextDate.setDate(nextDate.getDate() + 14);
        break;
      case PayrollPeriod.MONTHLY:
        nextDate.setMonth(nextDate.getMonth() + 1);
        break;
      case PayrollPeriod.QUARTERLY:
        nextDate.setMonth(nextDate.getMonth() + 3);
        break;
    }

    return nextDate;
  }

  async getPayrollCalendar(year: number) {
    const calendar = [];
    const startDate = new Date(year, 0, 1); // January 1st

    for (let month = 0; month < 12; month++) {
      const monthStart = new Date(year, month, 1);
      const monthEnd = new Date(year, month + 1, 0);
      const payrollDate = new Date(year, month + 1, 5); // 5th of next month

      let status: 'upcoming' | 'current' | 'completed';
      if (payrollDate < new Date()) {
        status = 'completed';
      } else if (payrollDate.getMonth() === new Date().getMonth()) {
        status = 'current';
      } else {
        status = 'upcoming';
      }

      calendar.push({
        period: PayrollPeriod.MONTHLY,
        startDate: monthStart,
        endDate: monthEnd,
        payrollDate,
        status
      });
    }

    return calendar;
  }

  private calculateRewardBreakdown(rewards: any[]): PayrollRewardBreakdown[] {
    const breakdown = new Map<RewardType, { count: number; totalAmount: number; rewardIds: string[] }>();

    rewards.forEach(reward => {
      const current = breakdown.get(reward.type) || { count: 0, totalAmount: 0, rewardIds: [] };
      breakdown.set(reward.type, {
        count: current.count + 1,
        totalAmount: current.totalAmount + reward.amount,
        rewardIds: [...current.rewardIds, reward.id]
      });
    });

    return Array.from(breakdown.entries()).map(([rewardType, data]) => ({
      rewardType,
      count: data.count,
      totalAmount: data.totalAmount,
      averageAmount: data.totalAmount / data.count,
      rewardIds: data.rewardIds
    }));
  }

  private calculatePayrollSummary(entries: PayrollEntry[]): PayrollSummary {
    const totalAmount = entries.reduce((sum, e) => sum + e.totalAmount, 0);
    const participantCount = entries.length;
    const averageAmount = participantCount > 0 ? totalAmount / participantCount : 0;

    // Reward type breakdown
    const rewardTypeBreakdown = Object.values(RewardType).map(type => {
      const typeEntries = entries.filter(e => 
        e.rewardBreakdown.some(rb => rb.rewardType === type)
      );
      
      const typeAmount = typeEntries.reduce((sum, e) => {
        const typeBreakdown = e.rewardBreakdown.find(rb => rb.rewardType === type);
        return sum + (typeBreakdown?.totalAmount || 0);
      }, 0);

      const typeCount = typeEntries.reduce((sum, e) => {
        const typeBreakdown = e.rewardBreakdown.find(rb => rb.rewardType === type);
        return sum + (typeBreakdown?.count || 0);
      }, 0);

      return {
        type,
        count: typeCount,
        totalAmount: typeAmount,
        participantCount: typeEntries.length
      };
    });

    // Top earners
    const topEarners = entries
      .sort((a, b) => b.totalAmount - a.totalAmount)
      .slice(0, 5)
      .map(e => ({
        userId: e.userId,
        userName: `User ${e.userId}`, // In real implementation, fetch actual name
        amount: e.totalAmount
      }));

    return {
      totalAmount,
      participantCount,
      averageAmount,
      rewardTypeBreakdown,
      topEarners
    };
  }

  private generateId(prefix: string): string {
    return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private async loadBatchesFromStorage(): Promise<void> {
    // In a real implementation, this would load from a database
  }

  private async loadConfigFromStorage(): Promise<void> {
    // In a real implementation, this would load from a database or config file
  }

  private async saveConfigToStorage(): Promise<void> {
    // In a real implementation, this would save to a database or config file
  }
}