import { prisma } from '../../../config/database';
import { logger } from '../../../utils/logger';
import { createHash } from 'crypto';
import { Decimal } from '@prisma/client/runtime/library';
import { geoipService } from '../../../services/geoip.service';

export class VideoAnalyticsService {
  /**
   * Hash IP address for privacy (SHA-256)
   */
  private hashIpAddress(ipAddress: string): string {
    return createHash('sha256').update(ipAddress).digest('hex');
  }

  /**
   * Hash user agent for privacy (SHA-256, truncate version numbers first)
   */
  private hashUserAgent(userAgent: string): string {
    // Truncate version numbers for better privacy
    const truncated = userAgent
      .replace(/\d+\.\d+\.\d+/g, 'X.X.X') // Remove version numbers
      .slice(0, 200); // Limit length
    return createHash('sha256').update(truncated).digest('hex');
  }

  /**
   * Record a new video view
   */
  async recordView(params: {
    videoId: number;
    userId?: string;
    ipAddress?: string;
    userAgent?: string;
    referer?: string;
  }): Promise<number> {
    const { videoId, userId, ipAddress, userAgent, referer } = params;

    try {
      // Resolve geo before hashing (IP is discarded after this block)
      const geo = ipAddress ? await geoipService.lookup(ipAddress) : null;

      const view = await prisma.videoView.create({
        data: {
          videoId,
          userId: userId || null,
          ipAddressHash: ipAddress ? this.hashIpAddress(ipAddress) : null,
          userAgentHash: userAgent ? this.hashUserAgent(userAgent) : null,
          referer: referer || null,
          country: geo?.country ?? null,
          region: geo?.region ?? null,
          city: geo?.city ?? null,
          latitude: geo?.latitude ?? null,
          longitude: geo?.longitude ?? null,
          watchTimeSeconds: 0,
          completed: false,
        },
      });

      // Increment view counter on video
      await prisma.video.update({
        where: { id: videoId },
        data: {
          viewCount: {
            increment: 1,
          },
        },
      });

      logger.info(`Recorded view for video ${videoId}`, { viewId: view.id });
      return view.id;
    } catch (error) {
      logger.error('Failed to record video view', { error, videoId });
      throw error;
    }
  }

  /**
   * Record a video event (play, pause, seek, complete)
   */
  async recordEvent(params: {
    videoId: number;
    viewId?: number;
    eventType: 'play' | 'pause' | 'seek' | 'complete';
    timestamp: number;
  }): Promise<void> {
    const { videoId, viewId, eventType, timestamp } = params;

    try {
      await prisma.videoEvent.create({
        data: {
          videoId,
          viewId: viewId || null,
          eventType,
          timestamp: new Decimal(timestamp),
        },
      });

      // If complete event, mark view as completed and increment finishCount
      if (eventType === 'complete' && viewId) {
        await prisma.videoView.update({
          where: { id: viewId },
          data: { completed: true },
        });

        await prisma.video.update({
          where: { id: videoId },
          data: {
            finishCount: {
              increment: 1,
            },
          },
        });
      }
    } catch (error) {
      logger.error('Failed to record video event', { error, videoId, eventType });
      // Don't throw - we don't want analytics failures to break playback
    }
  }

  /**
   * Update watch time for a view (called periodically during playback)
   */
  async updateWatchTime(viewId: number, watchTimeSeconds: number): Promise<void> {
    try {
      await prisma.videoView.update({
        where: { id: viewId },
        data: {
          watchTimeSeconds,
          updatedAt: new Date(),
        },
      });
    } catch (error) {
      logger.error('Failed to update watch time', { error, viewId });
      // Don't throw - we don't want analytics failures to break playback
    }
  }

  /**
   * Aggregate analytics for a video
   */
  async aggregateVideoAnalytics(videoId: number): Promise<void> {
    try {
      const [views, uniqueViewers, totalWatchTime, avgWatchTime, completions] = await Promise.all([
        // Total views
        prisma.videoView.count({
          where: { videoId },
        }),
        // Unique viewers (registered users only)
        prisma.videoView.findMany({
          where: {
            videoId,
            userId: { not: null },
          },
          distinct: ['userId'],
          select: { userId: true },
        }).then(views => views.length),
        // Total watch time
        prisma.videoView.aggregate({
          where: { videoId },
          _sum: {
            watchTimeSeconds: true,
          },
        }),
        // Average watch time
        prisma.videoView.aggregate({
          where: { videoId },
          _avg: {
            watchTimeSeconds: true,
          },
        }),
        // Completions
        prisma.videoView.count({
          where: {
            videoId,
            completed: true,
          },
        }),
      ]);

      // Get video duration for completion rate calculation
      const video = await prisma.video.findUnique({
        where: { id: videoId },
        select: { durationSeconds: true },
      });

      const completionRate =
        views > 0 && video?.durationSeconds
          ? new Decimal((completions / views) * 100)
          : new Decimal(0);

      // Update video analytics fields
      await prisma.video.update({
        where: { id: videoId },
        data: {
          uniqueViewers,
          totalWatchTimeSeconds: totalWatchTime._sum.watchTimeSeconds || 0,
          averageWatchTimeSeconds: avgWatchTime._avg.watchTimeSeconds
            ? new Decimal(avgWatchTime._avg.watchTimeSeconds)
            : new Decimal(0),
          completionRate,
        },
      });

      logger.info(`Aggregated analytics for video ${videoId}`, {
        views,
        uniqueViewers,
        completionRate: completionRate.toNumber(),
      });
    } catch (error) {
      logger.error('Failed to aggregate video analytics', { error, videoId });
      throw error;
    }
  }

  /**
   * Get detailed analytics for a video
   */
  async getVideoAnalytics(
    videoId: number,
    startDate?: Date,
    endDate?: Date
  ): Promise<{
    overview: {
      totalViews: number;
      uniqueViewers: number;
      averageWatchTime: number;
      completionRate: number;
      totalWatchTime: number;
    };
    viewsOverTime: Array<{ date: string; count: number }>;
    topReferrers: Array<{ referer: string; count: number }>;
    registeredViewers: Array<{
      userId: string;
      userName: string | null;
      userEmail: string;
      watchTime: number;
      completed: boolean;
      viewedAt: Date;
    }>;
  }> {
    const where: any = { videoId };
    if (startDate || endDate) {
      where.createdAt = {};
      if (startDate) where.createdAt.gte = startDate;
      if (endDate) where.createdAt.lte = endDate;
    }

    try {
      // Overview stats
      const video = await prisma.video.findUnique({
        where: { id: videoId },
        select: {
          viewCount: true,
          uniqueViewers: true,
          totalWatchTimeSeconds: true,
          averageWatchTimeSeconds: true,
          completionRate: true,
        },
      });

      if (!video) {
        throw new Error('Video not found');
      }

      // Views over time (last 30 days, grouped by day)
      const thirtyDaysAgo = new Date();
      thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

      const viewsByDay = await prisma.$queryRaw<Array<{ date: string; count: bigint }>>`
        SELECT
          DATE(created_at) as date,
          COUNT(*) as count
        FROM video_views
        WHERE video_id = ${videoId}
          AND created_at >= ${thirtyDaysAgo}
        GROUP BY DATE(created_at)
        ORDER BY DATE(created_at) ASC
      `;

      // Top referrers
      const topReferrers = await prisma.videoView.groupBy({
        by: ['referer'],
        where: {
          videoId,
          referer: { not: null },
        },
        _count: {
          referer: true,
        },
        orderBy: {
          _count: {
            referer: 'desc',
          },
        },
        take: 10,
      });

      // Registered viewers
      const registeredViewers = await prisma.videoView.findMany({
        where: {
          videoId,
          userId: { not: null },
        },
        include: {
          user: {
            select: {
              id: true,
              name: true,
              email: true,
            },
          },
        },
        orderBy: {
          createdAt: 'desc',
        },
        take: 100,
      });

      return {
        overview: {
          totalViews: video.viewCount,
          uniqueViewers: video.uniqueViewers,
          averageWatchTime: video.averageWatchTimeSeconds.toNumber(),
          completionRate: video.completionRate.toNumber(),
          totalWatchTime: video.totalWatchTimeSeconds,
        },
        viewsOverTime: viewsByDay.map((row) => ({
          date: row.date,
          count: Number(row.count),
        })),
        topReferrers: topReferrers.map((item) => ({
          referer: item.referer || 'Direct',
          count: item._count.referer,
        })),
        registeredViewers: registeredViewers.map((view) => ({
          userId: view.userId!,
          userName: view.user?.name || null,
          userEmail: view.user?.email || '',
          watchTime: view.watchTimeSeconds,
          completed: view.completed,
          viewedAt: view.createdAt,
        })),
      };
    } catch (error) {
      logger.error('Failed to get video analytics', { error, videoId });
      throw error;
    }
  }

  /**
   * Get top videos by a metric
   */
  async getTopVideos(
    metric: 'views' | 'watchTime',
    limit: number = 10
  ): Promise<
    Array<{
      id: number;
      title: string | null;
      value: number;
    }>
  > {
    try {
      const orderBy = metric === 'views' ? { viewCount: 'desc' as const } : { totalWatchTimeSeconds: 'desc' as const };

      const videos = await prisma.video.findMany({
        select: {
          id: true,
          title: true,
          viewCount: true,
          totalWatchTimeSeconds: true,
        },
        orderBy,
        take: limit,
      });

      return videos.map((video) => ({
        id: video.id,
        title: video.title,
        value: metric === 'views' ? video.viewCount : video.totalWatchTimeSeconds,
      }));
    } catch (error) {
      logger.error('Failed to get top videos', { error, metric });
      throw error;
    }
  }

  /**
   * Reset analytics for a video
   */
  async resetAnalytics(videoId: number): Promise<void> {
    try {
      // Delete all views and events
      await prisma.videoView.deleteMany({
        where: { videoId },
      });

      await prisma.videoEvent.deleteMany({
        where: { videoId },
      });

      // Reset counters
      await prisma.video.update({
        where: { id: videoId },
        data: {
          viewCount: 0,
          uniqueViewers: 0,
          totalWatchTimeSeconds: 0,
          averageWatchTimeSeconds: new Decimal(0),
          completionRate: new Decimal(0),
          finishCount: 0,
        },
      });

      logger.info(`Reset analytics for video ${videoId}`);
    } catch (error) {
      logger.error('Failed to reset analytics', { error, videoId });
      throw error;
    }
  }
}

export const videoAnalyticsService = new VideoAnalyticsService();
