import {prisma} from '../database/prisma.js';
import {redis} from '../database/redis.js';
import {Keys} from './keys.js';

/**
 * Time series data point for analytics
 */
export interface TimeSeriesDataPoint {
  date: string;
  emails: number;
  opens: number;
  clicks: number;
  bounces: number;
  delivered: number;
}

/**
 * Analytics Service
 *
 * PERFORMANCE CONSIDERATIONS:
 * - Aggregates data by day to reduce row count
 * - Uses Redis caching with 15-minute TTL
 * - Limits date ranges to prevent expensive queries
 * - Uses indexed fields (createdAt, projectId) for efficient filtering
 * - For 1M+ emails, consider background jobs for pre-aggregation
 */
export class AnalyticsService {
  private static readonly DEFAULT_DAYS_BACK = 30;
  private static readonly MAX_DAYS_BACK = 90;
  private static readonly TIMESERIES_CACHE_TTL = 900; // 15 minutes

  /**
   * Get time series data for email analytics
   *
   * Performance: O(n) where n = number of emails in date range
   * - Groups emails by day using SQL aggregation
   * - Cached in Redis for 15 minutes
   * - Limited to 90 days max to prevent performance issues
   *
   * For higher scale (1M+ emails/day), consider:
   * - Pre-aggregated daily stats table updated by background job
   * - Materialized view with daily refresh
   * - Time-series database (TimescaleDB, InfluxDB)
   */
  public static async getTimeSeriesData(
    projectId: string,
    startDate?: Date,
    endDate?: Date,
  ): Promise<TimeSeriesDataPoint[]> {
    // Calculate date range with limits
    const now = new Date();
    const effectiveEndDate = endDate || now;
    const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000);
    const effectiveStartDate = startDate || defaultStartDate;

    // Enforce max date range
    const maxStartDate = new Date(now.getTime() - this.MAX_DAYS_BACK * 24 * 60 * 60 * 1000);
    const limitedStartDate = effectiveStartDate < maxStartDate ? maxStartDate : effectiveStartDate;

    // Check cache first
    const cacheKey = Keys.Analytics.timeseries(
      projectId,
      limitedStartDate.toISOString(),
      effectiveEndDate.toISOString(),
    );
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    const result = await prisma.$queryRaw<
      {
        date: Date;
        total_emails: bigint;
        total_opens: bigint;
        total_clicks: bigint;
        total_bounces: bigint;
        total_delivered: bigint;
      }[]
    >`
			SELECT
				DATE_TRUNC('day', "createdAt") as date,
				COUNT(*) as total_emails,
				COUNT(CASE WHEN "openedAt" IS NOT NULL THEN 1 END) as total_opens,
				COUNT(CASE WHEN "clickedAt" IS NOT NULL THEN 1 END) as total_clicks,
				COUNT(CASE WHEN "bouncedAt" IS NOT NULL THEN 1 END) as total_bounces,
				COUNT(CASE WHEN "deliveredAt" IS NOT NULL THEN 1 END) as total_delivered
			FROM "emails"
			WHERE "projectId" = ${projectId}
				AND "createdAt" >= ${limitedStartDate}
				AND "createdAt" <= ${effectiveEndDate}
			GROUP BY DATE_TRUNC('day', "createdAt")
			ORDER BY date ASC
		`;

    // Convert to TimeSeriesDataPoint format
    const timeSeries: TimeSeriesDataPoint[] = result.map(row => ({
      date: row.date.toISOString(),
      emails: Number(row.total_emails),
      opens: Number(row.total_opens),
      clicks: Number(row.total_clicks),
      bounces: Number(row.total_bounces),
      delivered: Number(row.total_delivered),
    }));

    // Fill in missing dates with zero values
    const filledTimeSeries = this.fillMissingDates(timeSeries, limitedStartDate, effectiveEndDate);

    // Cache for 15 minutes
    await redis.setex(cacheKey, this.TIMESERIES_CACHE_TTL, JSON.stringify(filledTimeSeries));

    return filledTimeSeries;
  }

  /**
   * Get campaign performance metrics
   * Returns top performing campaigns by open rate
   */
  public static async getTopCampaigns(
    projectId: string,
    limit = 10,
    startDate?: Date,
    endDate?: Date,
  ): Promise<
    {
      id: string;
      subject: string;
      sentCount: number;
      openedCount: number;
      clickedCount: number;
      openRate: number;
      clickRate: number;
    }[]
  > {
    const now = new Date();
    const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000);

    const campaigns = await prisma.campaign.findMany({
      where: {
        projectId,
        sentAt: {
          gte: startDate || defaultStartDate,
          ...(endDate ? {lte: endDate} : {}),
        },
        status: 'SENT',
      },
      select: {
        id: true,
        subject: true,
        sentCount: true,
        openedCount: true,
        clickedCount: true,
      },
      orderBy: {
        openedCount: 'desc',
      },
      take: limit,
    });

    return campaigns.map(campaign => ({
      id: campaign.id,
      subject: campaign.subject || 'No subject',
      sentCount: campaign.sentCount || 0,
      openedCount: campaign.openedCount || 0,
      clickedCount: campaign.clickedCount || 0,
      openRate: campaign.sentCount ? ((campaign.openedCount || 0) / campaign.sentCount) * 100 : 0,
      clickRate: campaign.sentCount ? ((campaign.clickedCount || 0) / campaign.sentCount) * 100 : 0,
    }));
  }

  /**
   * Get campaign statistics overview
   * Returns aggregate stats for all campaigns in date range
   *
   * Performance: O(1) with indexed queries
   * - Uses aggregation on indexed fields
   * - Cached in Redis for 15 minutes
   */
  public static async getCampaignStats(
    projectId: string,
    startDate?: Date,
    endDate?: Date,
  ): Promise<{
    total: number;
    active: number;
    completed: number;
    averageOpenRate: number;
    averageClickRate: number;
  }> {
    const now = new Date();
    const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000);
    const effectiveStartDate = startDate || defaultStartDate;
    const effectiveEndDate = endDate || now;

    // Check cache
    const cacheKey = Keys.Analytics.campaignStats(
      projectId,
      effectiveStartDate.toISOString(),
      effectiveEndDate.toISOString(),
    );
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Get all campaigns in date range
    const [totalCampaigns, activeCampaigns, sentCampaigns] = await Promise.all([
      // Total campaigns created in date range
      prisma.campaign.count({
        where: {
          projectId,
          createdAt: {
            gte: effectiveStartDate,
            lte: effectiveEndDate,
          },
        },
      }),
      // Active campaigns (not sent yet)
      prisma.campaign.count({
        where: {
          projectId,
          createdAt: {
            gte: effectiveStartDate,
            lte: effectiveEndDate,
          },
          status: {
            in: ['DRAFT', 'SCHEDULED'],
          },
        },
      }),
      // Sent campaigns with stats
      prisma.campaign.findMany({
        where: {
          projectId,
          sentAt: {
            gte: effectiveStartDate,
            lte: effectiveEndDate,
          },
          status: 'SENT',
        },
        select: {
          sentCount: true,
          openedCount: true,
          clickedCount: true,
        },
      }),
    ]);

    // Calculate averages
    let totalSent = 0;
    let totalOpened = 0;
    let totalClicked = 0;

    sentCampaigns.forEach(campaign => {
      totalSent += campaign.sentCount || 0;
      totalOpened += campaign.openedCount || 0;
      totalClicked += campaign.clickedCount || 0;
    });

    const completed = sentCampaigns.length;
    const averageOpenRate = totalSent > 0 ? (totalOpened / totalSent) * 100 : 0;
    const averageClickRate = totalSent > 0 ? (totalClicked / totalSent) * 100 : 0;

    const stats = {
      total: totalCampaigns,
      active: activeCampaigns,
      completed,
      averageOpenRate: Math.round(averageOpenRate * 10) / 10, // Round to 1 decimal
      averageClickRate: Math.round(averageClickRate * 10) / 10,
    };

    // Cache for 15 minutes
    await redis.setex(cacheKey, this.TIMESERIES_CACHE_TTL, JSON.stringify(stats));

    return stats;
  }

  /**
   * Get top events by frequency with trend data
   *
   * Performance: O(n log n) where n = number of unique events
   * - Groups events and counts occurrences
   * - Compares with previous period for trend calculation
   * - Cached in Redis for 15 minutes
   */
  public static async getTopEvents(
    projectId: string,
    limit = 5,
    startDate?: Date,
    endDate?: Date,
  ): Promise<
    {
      name: string;
      count: number;
      trend: number;
    }[]
  > {
    const now = new Date();
    const defaultStartDate = new Date(now.getTime() - this.DEFAULT_DAYS_BACK * 24 * 60 * 60 * 1000);
    const effectiveStartDate = startDate || defaultStartDate;
    const effectiveEndDate = endDate || now;

    // Check cache
    const cacheKey = Keys.Analytics.topEvents(
      projectId,
      limit,
      effectiveStartDate.toISOString(),
      effectiveEndDate.toISOString(),
    );
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Calculate previous period for trend comparison
    const periodLength = effectiveEndDate.getTime() - effectiveStartDate.getTime();
    const previousStartDate = new Date(effectiveStartDate.getTime() - periodLength);
    const previousEndDate = new Date(effectiveStartDate.getTime());

    // Get current period events
    const currentEvents = await prisma.event.groupBy({
      by: ['name'],
      where: {
        projectId,
        createdAt: {
          gte: effectiveStartDate,
          lte: effectiveEndDate,
        },
      },
      _count: {
        id: true,
      },
      orderBy: {
        _count: {
          id: 'desc',
        },
      },
      take: limit,
    });

    // Get previous period events for trend calculation
    const previousEvents = await prisma.event.groupBy({
      by: ['name'],
      where: {
        projectId,
        name: {
          in: currentEvents.map(e => e.name),
        },
        createdAt: {
          gte: previousStartDate,
          lte: previousEndDate,
        },
      },
      _count: {
        id: true,
      },
    });

    // Create map of previous counts
    const previousCountMap = new Map(previousEvents.map(e => [e.name, e._count.id]));

    // Calculate trends
    const topEvents = currentEvents.map(event => {
      const currentCount = event._count.id;
      const previousCount = previousCountMap.get(event.name) || 0;

      // Calculate percentage change
      let trend = 0;
      if (previousCount > 0) {
        trend = Math.round(((currentCount - previousCount) / previousCount) * 100);
      } else if (currentCount > 0) {
        trend = 100; // New event, 100% increase
      }

      return {
        name: event.name,
        count: currentCount,
        trend,
      };
    });

    // Cache for 15 minutes
    await redis.setex(cacheKey, this.TIMESERIES_CACHE_TTL, JSON.stringify(topEvents));

    return topEvents;
  }

  /**
   * Fill in missing dates in time series with zero values
   * Ensures consistent daily data points even when no emails were sent
   */
  private static fillMissingDates(data: TimeSeriesDataPoint[], startDate: Date, endDate: Date): TimeSeriesDataPoint[] {
    const result: TimeSeriesDataPoint[] = [];
    const dataMap = new Map(data.map(point => [new Date(point.date).toDateString(), point]));

    // Iterate through each day in range
    const currentDate = new Date(startDate);
    currentDate.setHours(0, 0, 0, 0);
    const end = new Date(endDate);
    end.setHours(23, 59, 59, 999);

    while (currentDate <= end) {
      const dateKey = currentDate.toDateString();
      const existingData = dataMap.get(dateKey);

      if (existingData) {
        result.push(existingData);
      } else {
        // Fill with zeros for days with no data
        result.push({
          date: new Date(currentDate).toISOString(),
          emails: 0,
          opens: 0,
          clicks: 0,
          bounces: 0,
          delivered: 0,
        });
      }

      // Move to next day
      currentDate.setDate(currentDate.getDate() + 1);
    }

    return result;
  }
}
