import { prisma } from '../../config/database';
import type { Prisma } from '@prisma/client';
import type { CreateAdInput, UpdateAdInput, ListAdsInput } from './gallery-ads.schemas';

interface ActiveAdsContext {
  isAuthenticated: boolean;
  hasActiveSubscription: boolean;
  placement?: string;
}

class GalleryAdsService {
  /** Admin: paginated list of all ads */
  async listAll(filters: ListAdsInput) {
    const { page, limit, type, isActive } = filters;
    const where: Prisma.AdWhereInput = {};

    if (type) where.type = type;
    if (isActive !== undefined) where.isActive = isActive === 'true';

    const [ads, total] = await Promise.all([
      prisma.ad.findMany({
        where,
        orderBy: [{ position: 'asc' }, { createdAt: 'desc' }],
        skip: (page - 1) * limit,
        take: limit,
      }),
      prisma.ad.count({ where }),
    ]);

    return {
      ads,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /** Admin: get single ad */
  async getById(id: number) {
    return prisma.ad.findUnique({ where: { id } });
  }

  /** Admin: create a new ad */
  async create(data: CreateAdInput) {
    return prisma.ad.create({
      data: {
        type: data.type,
        variant: data.variant,
        title: data.title,
        subtitle: data.subtitle ?? null,
        imagePath: data.imagePath ?? null,
        linkUrl: data.linkUrl ?? null,
        ctaText: data.ctaText ?? null,
        ctaStyle: data.ctaStyle,
        bgColor: data.bgColor ?? null,
        iconEmoji: data.iconEmoji ?? null,
        visibility: data.visibility,
        isActive: data.isActive,
        position: data.position,
        frequency: data.frequency,
        startDate: data.startDate ? new Date(data.startDate) : null,
        endDate: data.endDate ? new Date(data.endDate) : null,
        placements: data.placements ?? [],
        isSystemAd: false,
      },
    });
  }

  /** Admin: update an ad */
  async update(id: number, data: UpdateAdInput) {
    const existing = await prisma.ad.findUnique({ where: { id } });
    if (!existing) return null;

    // Block type change on system ads
    if (existing.isSystemAd && data.type && data.type !== existing.type) {
      throw new Error('Cannot change type of a system ad');
    }

    const updateData: Prisma.AdUncheckedUpdateInput = {};

    if (data.type !== undefined) updateData.type = data.type;
    if (data.variant !== undefined) updateData.variant = data.variant;
    if (data.title !== undefined) updateData.title = data.title;
    if (data.subtitle !== undefined) updateData.subtitle = data.subtitle;
    if (data.imagePath !== undefined) updateData.imagePath = data.imagePath;
    if (data.linkUrl !== undefined) updateData.linkUrl = data.linkUrl;
    if (data.ctaText !== undefined) updateData.ctaText = data.ctaText;
    if (data.ctaStyle !== undefined) updateData.ctaStyle = data.ctaStyle;
    if (data.bgColor !== undefined) updateData.bgColor = data.bgColor;
    if (data.iconEmoji !== undefined) updateData.iconEmoji = data.iconEmoji;
    if (data.visibility !== undefined) updateData.visibility = data.visibility;
    if (data.isActive !== undefined) updateData.isActive = data.isActive;
    if (data.position !== undefined) updateData.position = data.position;
    if (data.frequency !== undefined) updateData.frequency = data.frequency;
    if (data.startDate !== undefined) updateData.startDate = data.startDate ? new Date(data.startDate) : null;
    if (data.endDate !== undefined) updateData.endDate = data.endDate ? new Date(data.endDate) : null;
    if (data.placements !== undefined) updateData.placements = data.placements;

    updateData.updatedAt = new Date();

    return prisma.ad.update({ where: { id }, data: updateData });
  }

  /** Admin: delete an ad (blocked for system ads) */
  async delete(id: number) {
    const existing = await prisma.ad.findUnique({ where: { id } });
    if (!existing) return null;
    if (existing.isSystemAd) {
      throw new Error('Cannot delete a system ad');
    }
    return prisma.ad.delete({ where: { id } });
  }

  /** Admin: bulk reorder ads by position */
  async reorder(ids: number[]) {
    const ops = ids.map((id, index) =>
      prisma.ad.update({
        where: { id },
        data: { position: index, updatedAt: new Date() },
      })
    );
    await prisma.$transaction(ops);
  }

  /** Public: get active ads filtered by visibility, schedule, and feature gates */
  async getActiveAds(context: ActiveAdsContext) {
    const now = new Date();

    // Check if gallery ads feature is enabled
    const settings = await prisma.siteSettings.findFirst();
    if (!settings?.enableGalleryAds) return [];

    const ads = await prisma.ad.findMany({
      where: {
        isActive: true,
        OR: [
          { startDate: null },
          { startDate: { lte: now } },
        ],
      },
      orderBy: { position: 'asc' },
      take: 100, // Safety cap: post-fetch filter may reduce further
    });

    // Filter in application layer for complex logic
    return ads.filter((ad) => {
      // End date check
      if (ad.endDate && ad.endDate < now) return false;

      // Visibility check
      switch (ad.visibility) {
        case 'anonymous':
          if (context.isAuthenticated) return false;
          break;
        case 'authenticated':
          if (!context.isAuthenticated) return false;
          break;
        case 'non_subscriber':
          if (!context.isAuthenticated || context.hasActiveSubscription) return false;
          break;
        // 'everyone' always passes
      }

      // Payment-type ads only if payments enabled
      if (['payment_subscribe', 'payment_donate', 'payment_shop'].includes(ad.type)) {
        if (!settings.enablePayments) return false;
      }

      // Placement filter: empty placements array = show on all pages
      if (context.placement) {
        const adPlacements = (ad.placements as string[]) || [];
        if (adPlacements.length > 0 && !adPlacements.includes(context.placement)) {
          return false;
        }
      }

      return true;
    });
  }

  /**
   * Ensure a Session record exists for FK integrity.
   * Follows the same upsert pattern used by media upvote/comment routes.
   */
  private async ensureSession(sessionId: string): Promise<void> {
    await prisma.session.upsert({
      where: { id: sessionId },
      create: { id: sessionId },
      update: { lastSeenAt: new Date() },
    });
  }

  /** Public: increment impression count + create individual record */
  async recordImpression(adId: number, sessionId?: string, userId?: string) {
    if (sessionId) {
      await this.ensureSession(sessionId);
    }

    await prisma.$transaction([
      prisma.ad.update({
        where: { id: adId },
        data: { impressionCount: { increment: 1 } },
      }),
      prisma.adImpression.create({
        data: {
          adId,
          sessionId: sessionId ?? null,
          userId: userId ?? null,
        },
      }),
    ]);
  }

  /** Public: increment click count + create individual record */
  async recordClick(adId: number, sessionId?: string, userId?: string) {
    if (sessionId) {
      await this.ensureSession(sessionId);
    }

    await prisma.$transaction([
      prisma.ad.update({
        where: { id: adId },
        data: { clickCount: { increment: 1 } },
      }),
      prisma.adClick.create({
        data: {
          adId,
          sessionId: sessionId ?? null,
          userId: userId ?? null,
        },
      }),
    ]);
  }

  /**
   * Admin: get per-ad analytics with daily breakdown.
   * Counter fields on Ad remain for fast table reads; these individual records
   * enable time-series queries. For high-traffic galleries, consider periodic
   * cleanup of records older than 90 days (preserving counter totals).
   */
  async getAdAnalytics(adId: number, days: number = 30) {
    const since = new Date(Date.now() - days * 86400000);

    const [dailyImpressions, dailyClicks, uniqueSessions, ad] = await Promise.all([
      prisma.$queryRaw<{ date: string; count: bigint }[]>`
        SELECT DATE("created_at") as date, COUNT(*) as count
        FROM ad_impressions
        WHERE ad_id = ${adId} AND created_at >= ${since}
        GROUP BY DATE("created_at")
        ORDER BY date
      `,
      prisma.$queryRaw<{ date: string; count: bigint }[]>`
        SELECT DATE("created_at") as date, COUNT(*) as count
        FROM ad_clicks
        WHERE ad_id = ${adId} AND created_at >= ${since}
        GROUP BY DATE("created_at")
        ORDER BY date
      `,
      prisma.adImpression.findMany({
        where: { adId, createdAt: { gte: since }, sessionId: { not: null } },
        distinct: ['sessionId'],
        select: { sessionId: true },
      }),
      prisma.ad.findUnique({
        where: { id: adId },
        select: { impressionCount: true, clickCount: true },
      }),
    ]);

    // Merge daily impressions + clicks into a single array
    const dateMap = new Map<string, { impressions: number; clicks: number }>();

    for (const row of dailyImpressions) {
      const dateStr = new Date(row.date).toISOString().split('T')[0];
      const entry = dateMap.get(dateStr) || { impressions: 0, clicks: 0 };
      entry.impressions = Number(row.count);
      dateMap.set(dateStr, entry);
    }

    for (const row of dailyClicks) {
      const dateStr = new Date(row.date).toISOString().split('T')[0];
      const entry = dateMap.get(dateStr) || { impressions: 0, clicks: 0 };
      entry.clicks = Number(row.count);
      dateMap.set(dateStr, entry);
    }

    const daily = Array.from(dateMap.entries())
      .map(([date, counts]) => ({ date, ...counts }))
      .sort((a, b) => a.date.localeCompare(b.date));

    const totalImpressions = ad?.impressionCount ?? 0;
    const totalClicks = ad?.clickCount ?? 0;
    const ctr = totalImpressions > 0 ? Number(((totalClicks / totalImpressions) * 100).toFixed(1)) : 0;

    return {
      daily,
      totals: {
        impressions: totalImpressions,
        clicks: totalClicks,
        uniqueSessions: uniqueSessions.length,
        ctr,
      },
    };
  }
  /**
   * Admin: aggregate analytics across all ads.
   * Returns daily breakdown, totals, top ads, and per-placement stats.
   */
  async getAggregateAnalytics(days: number = 30) {
    const since = new Date(Date.now() - days * 86400000);

    const [dailyImpressions, dailyClicks, uniqueSessions, topAdsRaw] = await Promise.all([
      prisma.$queryRaw<{ date: string; count: bigint }[]>`
        SELECT DATE("created_at") as date, COUNT(*) as count
        FROM ad_impressions
        WHERE created_at >= ${since}
        GROUP BY DATE("created_at")
        ORDER BY date
      `,
      prisma.$queryRaw<{ date: string; count: bigint }[]>`
        SELECT DATE("created_at") as date, COUNT(*) as count
        FROM ad_clicks
        WHERE created_at >= ${since}
        GROUP BY DATE("created_at")
        ORDER BY date
      `,
      prisma.adImpression.findMany({
        where: { createdAt: { gte: since }, sessionId: { not: null } },
        distinct: ['sessionId'],
        select: { sessionId: true },
      }),
      prisma.$queryRaw<{ id: number; title: string; type: string; impressions: bigint; clicks: bigint }[]>`
        SELECT a.id, a.title, a.type,
          a.impression_count as impressions,
          a.click_count as clicks
        FROM ads a
        ORDER BY a.impression_count DESC
        LIMIT 10
      `,
    ]);

    // Merge daily impressions + clicks into a single array
    const dateMap = new Map<string, { impressions: number; clicks: number }>();

    for (const row of dailyImpressions) {
      const dateStr = new Date(row.date).toISOString().split('T')[0];
      const entry = dateMap.get(dateStr) || { impressions: 0, clicks: 0 };
      entry.impressions = Number(row.count);
      dateMap.set(dateStr, entry);
    }

    for (const row of dailyClicks) {
      const dateStr = new Date(row.date).toISOString().split('T')[0];
      const entry = dateMap.get(dateStr) || { impressions: 0, clicks: 0 };
      entry.clicks = Number(row.count);
      dateMap.set(dateStr, entry);
    }

    const daily = Array.from(dateMap.entries())
      .map(([date, counts]) => ({ date, ...counts }))
      .sort((a, b) => a.date.localeCompare(b.date));

    // Totals from daily sums
    const totalImpressions = daily.reduce((sum, d) => sum + d.impressions, 0);
    const totalClicks = daily.reduce((sum, d) => sum + d.clicks, 0);
    const ctr = totalImpressions > 0 ? Number(((totalClicks / totalImpressions) * 100).toFixed(1)) : 0;

    // Top ads with CTR
    const topAds = topAdsRaw.map((row) => {
      const imp = Number(row.impressions);
      const clk = Number(row.clicks);
      return {
        id: row.id,
        title: row.title,
        type: row.type,
        impressions: imp,
        clicks: clk,
        ctr: imp > 0 ? Number(((clk / imp) * 100).toFixed(1)) : 0,
      };
    });

    // Placement breakdown: fetch active ads with placements
    const activeAds = await prisma.ad.findMany({
      where: { isActive: true },
      select: { placements: true, impressionCount: true, clickCount: true },
    });

    const placementMap = new Map<string, { adCount: number; impressions: number; clicks: number }>();
    const placementLabels: Record<string, string> = {
      gallery: 'Gallery',
      campaigns_list: 'Campaigns List',
      campaign_detail: 'Campaign Detail',
      shifts: 'Shifts',
      shop: 'Shop',
      donate: 'Donate',
      pricing: 'Pricing',
      landing_page: 'Landing Pages',
      docs: 'Documentation',
    };

    for (const ad of activeAds) {
      const placements = (ad.placements as string[]) || [];
      // Ads with no placements target all pages
      const keys = placements.length > 0 ? placements : Object.keys(placementLabels);
      for (const key of keys) {
        const entry = placementMap.get(key) || { adCount: 0, impressions: 0, clicks: 0 };
        entry.adCount++;
        entry.impressions += ad.impressionCount ?? 0;
        entry.clicks += ad.clickCount ?? 0;
        placementMap.set(key, entry);
      }
    }

    const byPlacement = Array.from(placementMap.entries()).map(([placement, stats]) => ({
      placement,
      label: placementLabels[placement] || placement,
      ...stats,
    }));

    return {
      daily,
      totals: { impressions: totalImpressions, clicks: totalClicks, uniqueSessions: uniqueSessions.length, ctr },
      topAds,
      byPlacement,
    };
  }
}

export const galleryAdsService = new GalleryAdsService();
