export class DataManager {
  constructor(mongodb, redis, timeManager, options) {
    this.mongodb = mongodb;
    this.redis = redis;
    this.timeManager = timeManager;
    this.options = options || {};
  }

  ensureDate(d) {
    if (d instanceof Date) return d;
    if (typeof d === 'string') return new Date(d);
    if (typeof d === 'number') return new Date(d);
    if (d && d.date) return new Date(d.date); // Handle MongoDB records
    console.error('Invalid date value:', d);
    return new Date(); // fallback to current date
  }

  async getData(dateRange, fetchFn) {
    try {
      // Get historical data from MongoDB
      const historicalData = await this.getHistoricalDays(dateRange.start, dateRange.end);
      
      // Find any missing date ranges
      const missingRanges = this.findMissingDateRanges(dateRange.start, dateRange.end, historicalData);

      // Fetch missing data
      for (const range of missingRanges) {
        const data = await fetchFn(range);
        await this.storeHistoricalPeriod(range.start, range.end, data);
      }

      // Get updated historical data
      const updatedData = await this.getHistoricalDays(dateRange.start, dateRange.end);
      
      // Handle both nested and flat data structures
      if (updatedData && updatedData.length > 0) {
        // Process each record and combine them
        const processedData = updatedData.map(record => {
          if (record.data) {
            return record.data;
          }
          if (record.total !== undefined) {
            return {
              total: record.total,
              by_direction: record.by_direction,
              by_status: record.by_status,
              by_missed_reason: record.by_missed_reason,
              by_hour: record.by_hour,
              by_users: record.by_users,
              daily_data: record.daily_data,
              duration_distribution: record.duration_distribution,
              average_duration: record.average_duration
            };
          }
          return null;
        }).filter(Boolean);

        // Combine the data
        if (processedData.length > 0) {
          return this.combineMetrics(processedData);
        }
      }
      
      // Otherwise process as raw call data
      return this.processCallData(updatedData);
    } catch (error) {
      console.error('Error in getData:', error);
      throw error;
    }
  }

  findMissingDateRanges(start, end, existingDates) {
    const missingRanges = [];
    const existingDatesSet = new Set(
      existingDates.map((d) => {
        // Handle both nested and flat data structures
        const date = d.date ? d.date : d;
        return this.ensureDate(date).toISOString().split("T")[0];
      })
    );

    let current = new Date(start);
    const endDate = new Date(end);
    
    while (current < endDate) {
      const dayBounds = this.timeManager.getDayBounds(current);
      const dayKey = dayBounds.start.toISOString().split("T")[0];

      if (!existingDatesSet.has(dayKey)) {
        // Found a missing day
        const missingStart = new Date(dayBounds.start);
        const missingEnd = new Date(dayBounds.end);

        missingRanges.push({
          start: missingStart,
          end: missingEnd,
        });
      }

      // Move to the next day using timeManager to ensure proper business day boundaries
      current = new Date(dayBounds.end.getTime() + 1);
    }

    return missingRanges;
  }

  async getCurrentDay(fetchFn) {
    const now = new Date();
    const todayBounds = this.timeManager.getDayBounds(now);
    const todayKey = this.timeManager.formatDate(todayBounds.start);
    const cacheKey = `${this.options.collection}:current_day:${todayKey}`;

    try {
      // Check cache first
      if (this.redis?.isOpen) {
        const cached = await this.redis.get(cacheKey);
        if (cached) {
          const parsedCache = JSON.parse(cached);
          if (parsedCache.total !== undefined) {
            // Use timeManager to check if the cached data is for today
            const cachedDate = new Date(parsedCache.daily_data[0].date);
            const isToday = this.timeManager.isToday(cachedDate);
            
            if (isToday) {
              return parsedCache;
            }
          }
        }
      }

      // Get safe end time that's never in the future
      const safeEnd = this.timeManager.getCurrentBusinessDayEnd();

      // Fetch and process current day data with safe end time
      const data = await fetchFn({
        start: todayBounds.start,
        end: safeEnd
      });

      if (!data) {
        return null;
      }

      // Cache the data with a shorter TTL for today's data
      if (this.redis?.isOpen) {
        const ttl = Math.min(
          this.options.redisTTL,
          60 * 5 // 5 minutes max for today's data
        );
        await this.redis.set(cacheKey, JSON.stringify(data), {
          EX: ttl,
        });
      }

      return data;
    } catch (error) {
      console.error('Error in getCurrentDay:', error);
      throw error;
    }
  }

  getDayCount(start, end) {
    // Calculate full days between dates using timeManager
    const startDay = this.timeManager.getDayBounds(start);
    const endDay = this.timeManager.getDayBounds(end);
    return Math.ceil((endDay.end - startDay.start) / (24 * 60 * 60 * 1000));
  }

  async fetchMissingDays(start, end, existingData, fetchFn) {
    const existingDates = new Set(
      existingData.map((d) => this.timeManager.formatDate(d.date))
    );
    const missingData = [];

    let currentDate = new Date(start);
    while (currentDate < end) {
      const dayBounds = this.timeManager.getDayBounds(currentDate);
      const dateString = this.timeManager.formatDate(dayBounds.start);
      
      if (!existingDates.has(dateString)) {
        const data = await fetchFn({
          start: dayBounds.start,
          end: dayBounds.end,
        });

        await this.storeHistoricalDay(dayBounds.start, data);
        missingData.push(data);
      }

      // Move to next day using timeManager to ensure proper business day boundaries
      currentDate = new Date(dayBounds.end.getTime() + 1);
    }

    return missingData;
  }

  async getHistoricalDays(start, end) {
    try {
      if (!this.mongodb) return [];

      const collection = this.mongodb.collection(this.options.collection);
      const startDay = this.timeManager.getDayBounds(start);
      const endDay = this.timeManager.getDayBounds(end);

      const records = await collection
        .find({
          date: {
            $gte: startDay.start,
            $lt: endDay.start,
          },
        })
        .sort({ date: 1 })
        .toArray();

      return records;
    } catch (error) {
      console.error('Error getting historical days:', error);
      return [];
    }
  }

  combineMetrics(metricsArray) {
    if (!metricsArray || metricsArray.length === 0) return null;
    if (metricsArray.length === 1) return metricsArray[0];

    const combined = {
      total: 0,
      by_direction: { inbound: 0, outbound: 0 },
      by_status: { answered: 0, missed: 0 },
      by_missed_reason: {},
      by_hour: Array(24).fill(0),
      by_users: {},
      daily_data: [],
      duration_distribution: [
        { range: '0-1m', count: 0 },
        { range: '1-5m', count: 0 },
        { range: '5-15m', count: 0 },
        { range: '15-30m', count: 0 },
        { range: '30m+', count: 0 }
      ],
      average_duration: 0
    };

    let totalAnswered = 0;
    let totalDuration = 0;

    metricsArray.forEach(metrics => {
      // Sum basic metrics
      combined.total += metrics.total;
      combined.by_direction.inbound += metrics.by_direction.inbound;
      combined.by_direction.outbound += metrics.by_direction.outbound;
      combined.by_status.answered += metrics.by_status.answered;
      combined.by_status.missed += metrics.by_status.missed;

      // Combine missed reasons
      Object.entries(metrics.by_missed_reason).forEach(([reason, count]) => {
        combined.by_missed_reason[reason] = (combined.by_missed_reason[reason] || 0) + count;
      });

      // Sum hourly data
      metrics.by_hour.forEach((count, hour) => {
        combined.by_hour[hour] += count;
      });

      // Combine user data
      Object.entries(metrics.by_users).forEach(([userId, userData]) => {
        if (!combined.by_users[userId]) {
          combined.by_users[userId] = {
            id: userData.id,
            name: userData.name,
            total: 0,
            answered: 0,
            missed: 0,
            total_duration: 0,
            average_duration: 0
          };
        }
        combined.by_users[userId].total += userData.total;
        combined.by_users[userId].answered += userData.answered;
        combined.by_users[userId].missed += userData.missed;
        combined.by_users[userId].total_duration += userData.total_duration || 0;
      });

      // Combine duration distribution
      metrics.duration_distribution.forEach((dist, index) => {
        combined.duration_distribution[index].count += dist.count;
      });

      // Accumulate for average duration calculation
      if (metrics.average_duration && metrics.by_status.answered) {
        totalDuration += metrics.average_duration * metrics.by_status.answered;
        totalAnswered += metrics.by_status.answered;
      }

      // Merge daily data
      if (metrics.daily_data) {
        combined.daily_data.push(...metrics.daily_data);
      }
    });

    // Calculate final average duration
    if (totalAnswered > 0) {
      combined.average_duration = Math.round(totalDuration / totalAnswered);
    }

    // Calculate user averages
    Object.values(combined.by_users).forEach(user => {
      if (user.answered > 0) {
        user.average_duration = Math.round(user.total_duration / user.answered);
      }
    });

    // Sort and deduplicate daily data
    combined.daily_data = Array.from(
      new Map(combined.daily_data.map(item => [item.date, item])).values()
    ).sort((a, b) => a.date.localeCompare(b.date));

    return combined;
  }
} 