import { Prisma, ResponseStatus } from '@prisma/client';
import { prisma } from '../../../config/database';
import type { EffectivenessQuery, TrendQuery, GeoQuery, RepQuery } from './effectiveness.schemas';

function buildDateFilter(query: EffectivenessQuery) {
  const filter: { gte?: Date; lte?: Date } = {};
  if (query.dateFrom) filter.gte = new Date(query.dateFrom);
  if (query.dateTo) filter.lte = new Date(query.dateTo);
  return Object.keys(filter).length > 0 ? filter : undefined;
}

export const effectivenessService = {
  /**
   * Per-campaign KPIs: email counts, response counts, response rate
   */
  async getOverviewStats(query: EffectivenessQuery) {
    const dateFilter = buildDateFilter(query);
    const campaignWhere: Prisma.CampaignWhereInput = {};
    if (query.campaignId) campaignWhere.id = query.campaignId;

    const emailWhere: Prisma.CampaignEmailWhereInput = {};
    if (query.campaignId) emailWhere.campaignId = query.campaignId;
    if (dateFilter) emailWhere.sentAt = dateFilter;

    const responseWhere: Prisma.RepresentativeResponseWhereInput = {};
    if (query.campaignId) responseWhere.campaignId = query.campaignId;
    if (dateFilter) responseWhere.createdAt = dateFilter;

    const callWhere: Prisma.CallWhereInput = {};
    if (query.campaignId) callWhere.campaignId = query.campaignId;
    if (dateFilter) callWhere.calledAt = dateFilter;

    const [campaigns, emailsByStatus, responsesByStatus, totalCalls, totalEmails, totalResponses] = await Promise.all([
      prisma.campaign.findMany({
        where: campaignWhere,
        select: {
          id: true,
          title: true,
          slug: true,
          status: true,
          createdAt: true,
          _count: { select: { emails: true, responses: true, calls: true } },
        },
        orderBy: { createdAt: 'desc' },
      }),
      prisma.campaignEmail.groupBy({
        by: ['campaignId', 'status'],
        where: emailWhere,
        _count: true,
      }),
      prisma.representativeResponse.groupBy({
        by: ['campaignId', 'status'],
        where: responseWhere,
        _count: true,
      }),
      prisma.call.count({ where: callWhere }),
      prisma.campaignEmail.count({ where: emailWhere }),
      prisma.representativeResponse.count({ where: { ...responseWhere, status: ResponseStatus.APPROVED } }),
    ]);

    // Build per-campaign email status map
    const emailStatusMap = new Map<string, Record<string, number>>();
    for (const row of emailsByStatus) {
      if (!emailStatusMap.has(row.campaignId)) {
        emailStatusMap.set(row.campaignId, {});
      }
      emailStatusMap.get(row.campaignId)![row.status] = row._count;
    }

    // Build per-campaign response status map
    const responseStatusMap = new Map<string, Record<string, number>>();
    for (const row of responsesByStatus) {
      if (!responseStatusMap.has(row.campaignId)) {
        responseStatusMap.set(row.campaignId, {});
      }
      responseStatusMap.get(row.campaignId)![row.status] = row._count;
    }

    const campaignStats = campaigns.map((c) => {
      const emailBreakdown = emailStatusMap.get(c.id) || {};
      const responseBreakdown = responseStatusMap.get(c.id) || {};
      const emailTotal = Object.values(emailBreakdown).reduce((a, b) => a + b, 0);
      const approvedResponses = responseBreakdown[ResponseStatus.APPROVED] || 0;
      const responseRate = emailTotal > 0 ? approvedResponses / emailTotal : 0;

      return {
        campaignId: c.id,
        title: c.title,
        slug: c.slug,
        status: c.status,
        createdAt: c.createdAt,
        emailTotal,
        emailBreakdown,
        responseTotal: Object.values(responseBreakdown).reduce((a, b) => a + b, 0),
        approvedResponses,
        responseBreakdown,
        responseRate,
        callCount: c._count.calls,
      };
    });

    const activeCampaigns = campaigns.filter((c) => c.status === 'ACTIVE').length;
    const avgResponseRate = totalEmails > 0 ? totalResponses / totalEmails : 0;

    return {
      summary: {
        totalEmails,
        totalResponses,
        totalCalls,
        activeCampaigns,
        totalCampaigns: campaigns.length,
        avgResponseRate,
      },
      campaigns: campaignStats,
    };
  },

  /**
   * Cross-campaign representative tracking: emails received, responses given, response rate
   */
  async getRepresentativeEffectiveness(query: RepQuery) {
    const dateFilter = buildDateFilter(query);

    const emailWhere: Prisma.CampaignEmailWhereInput = {};
    if (query.campaignId) emailWhere.campaignId = query.campaignId;
    if (dateFilter) emailWhere.sentAt = dateFilter;

    const responseWhere: Prisma.RepresentativeResponseWhereInput = {};
    if (query.campaignId) responseWhere.campaignId = query.campaignId;
    if (dateFilter) responseWhere.createdAt = dateFilter;

    const [emailsByRecipient, responsesByRep] = await Promise.all([
      prisma.campaignEmail.groupBy({
        by: ['recipientEmail', 'recipientName', 'recipientLevel'],
        where: emailWhere,
        _count: true,
      }),
      prisma.representativeResponse.groupBy({
        by: ['representativeName', 'representativeLevel'],
        where: { ...responseWhere, status: ResponseStatus.APPROVED },
        _count: true,
      }),
    ]);

    // Also count verified responses
    const verifiedByRep = await prisma.representativeResponse.groupBy({
      by: ['representativeName'],
      where: { ...responseWhere, isVerified: true },
      _count: true,
    });

    const verifiedMap = new Map(verifiedByRep.map((r) => [r.representativeName, r._count]));

    // Build response map keyed by rep name (normalized lowercase)
    const responseMap = new Map<string, { count: number; level: string }>();
    for (const row of responsesByRep) {
      responseMap.set(row.representativeName.toLowerCase(), {
        count: row._count,
        level: row.representativeLevel,
      });
    }

    // Merge: start from email recipients, enrich with response data
    const repMap = new Map<string, {
      name: string;
      email: string;
      level: string | null;
      emailsReceived: number;
      responsesGiven: number;
      verifiedCount: number;
      responseRate: number;
    }>();

    for (const row of emailsByRecipient) {
      const key = (row.recipientName || row.recipientEmail).toLowerCase();
      const existing = repMap.get(key);
      if (existing) {
        existing.emailsReceived += row._count;
      } else {
        const respData = responseMap.get(key);
        const verifiedCount = verifiedMap.get(row.recipientName || row.recipientEmail) || 0;
        repMap.set(key, {
          name: row.recipientName || row.recipientEmail,
          email: row.recipientEmail,
          level: row.recipientLevel,
          emailsReceived: row._count,
          responsesGiven: respData?.count || 0,
          verifiedCount,
          responseRate: 0,
        });
      }
    }

    // Also add reps who responded but weren't in email records
    for (const row of responsesByRep) {
      const key = row.representativeName.toLowerCase();
      if (!repMap.has(key)) {
        const verifiedCount = verifiedMap.get(row.representativeName) || 0;
        repMap.set(key, {
          name: row.representativeName,
          email: '',
          level: row.representativeLevel,
          emailsReceived: 0,
          responsesGiven: row._count,
          verifiedCount,
          responseRate: 0,
        });
      }
    }

    // Compute response rates
    const reps = Array.from(repMap.values()).map((r) => ({
      ...r,
      responseRate: r.emailsReceived > 0 ? r.responsesGiven / r.emailsReceived : 0,
    }));

    // Sort
    if (query.sortBy === 'responseRate') {
      reps.sort((a, b) => b.responseRate - a.responseRate);
    } else if (query.sortBy === 'name') {
      reps.sort((a, b) => a.name.localeCompare(b.name));
    } else {
      reps.sort((a, b) => b.responsesGiven - a.responsesGiven);
    }

    // Level distribution
    const levelCounts: Record<string, number> = {};
    for (const row of responsesByRep) {
      levelCounts[row.representativeLevel] = (levelCounts[row.representativeLevel] || 0) + row._count;
    }

    return {
      representatives: reps.slice(0, query.limit),
      totalRepresentatives: reps.length,
      levelDistribution: Object.entries(levelCounts).map(([level, count]) => ({ level, count })),
    };
  },

  /**
   * Engagement breakdown by geographic area (postal code, city, or province)
   */
  async getGeographicBreakdown(query: GeoQuery) {
    const dateFilter = buildDateFilter(query);

    const emailWhere: Prisma.CampaignEmailWhereInput = {
      userPostalCode: { not: null },
    };
    if (query.campaignId) emailWhere.campaignId = query.campaignId;
    if (dateFilter) emailWhere.sentAt = dateFilter;

    if (query.groupBy === 'postalCode') {
      const results = await prisma.campaignEmail.groupBy({
        by: ['userPostalCode'],
        where: emailWhere,
        _count: true,
        orderBy: { _count: { userPostalCode: 'desc' } },
        take: query.limit,
      });

      // Enrich with city/province from postal code cache
      const postalCodes = results
        .map((r) => r.userPostalCode)
        .filter((pc): pc is string => pc !== null);

      const cacheEntries = postalCodes.length > 0
        ? await prisma.postalCodeCache.findMany({
            where: { postalCode: { in: postalCodes } },
            select: { postalCode: true, city: true, province: true },
          })
        : [];

      const cacheMap = new Map(cacheEntries.map((e) => [e.postalCode, e]));

      return {
        groupBy: query.groupBy,
        data: results.map((r) => {
          const cache = cacheMap.get(r.userPostalCode || '');
          return {
            key: r.userPostalCode || 'Unknown',
            emailCount: r._count,
            city: cache?.city || null,
            province: cache?.province || null,
          };
        }),
      };
    }

    // For city/province grouping, we need to join with postal_code_cache
    // Use pre-validated lookup map to prevent SQL injection if enum expands
    const GROUP_COL_MAP: Record<string, ReturnType<typeof Prisma.sql>> = {
      province: Prisma.sql`pcc.province`,
      city: Prisma.sql`pcc.city`,
    };
    const groupCol = GROUP_COL_MAP[query.groupBy!] ?? GROUP_COL_MAP.city;
    const campaignFilter = query.campaignId
      ? Prisma.sql`AND ce."campaignId" = ${query.campaignId}`
      : Prisma.sql``;
    const dateGteFilter = dateFilter?.gte
      ? Prisma.sql`AND ce."sentAt" >= ${dateFilter.gte}`
      : Prisma.sql``;
    const dateLteFilter = dateFilter?.lte
      ? Prisma.sql`AND ce."sentAt" <= ${dateFilter.lte}`
      : Prisma.sql``;

    const rawResults = await prisma.$queryRaw<Array<{ key: string; email_count: bigint }>>`
      SELECT ${groupCol} as key, COUNT(*) as email_count
      FROM campaign_emails ce
      LEFT JOIN postal_code_cache pcc ON ce."userPostalCode" = pcc."postalCode"
      WHERE ce."userPostalCode" IS NOT NULL
        AND ${groupCol} IS NOT NULL
        ${campaignFilter}
        ${dateGteFilter}
        ${dateLteFilter}
      GROUP BY ${groupCol}
      ORDER BY email_count DESC
      LIMIT ${query.limit}
    `;

    return {
      groupBy: query.groupBy,
      data: rawResults.map((r) => ({
        key: r.key,
        emailCount: Number(r.email_count),
        city: null,
        province: null,
      })),
    };
  },

  /**
   * Conversion funnel: emails → unique participants → responses → verified responses
   */
  async getFunnelData(query: EffectivenessQuery) {
    const dateFilter = buildDateFilter(query);

    const emailWhere: Prisma.CampaignEmailWhereInput = {};
    if (query.campaignId) emailWhere.campaignId = query.campaignId;
    if (dateFilter) emailWhere.sentAt = dateFilter;

    const responseWhere: Prisma.RepresentativeResponseWhereInput = {};
    if (query.campaignId) responseWhere.campaignId = query.campaignId;
    if (dateFilter) responseWhere.createdAt = dateFilter;

    const callWhere: Prisma.CallWhereInput = {};
    if (query.campaignId) callWhere.campaignId = query.campaignId;
    if (dateFilter) callWhere.calledAt = dateFilter;

    // Build parameterized conditions for unique participant count
    const campaignFilter = query.campaignId
      ? Prisma.sql`AND "campaignId" = ${query.campaignId}`
      : Prisma.sql``;
    const dateGteFilter = dateFilter?.gte
      ? Prisma.sql`AND "sentAt" >= ${dateFilter.gte}`
      : Prisma.sql``;
    const dateLteFilter = dateFilter?.lte
      ? Prisma.sql`AND "sentAt" <= ${dateFilter.lte}`
      : Prisma.sql``;

    const [emailsSent, uniqueParticipants, approvedResponses, verifiedResponses, callsMade] = await Promise.all([
      prisma.campaignEmail.count({ where: emailWhere }),
      prisma.$queryRaw<[{ count: bigint }]>`
        SELECT COUNT(DISTINCT "userEmail") as count FROM campaign_emails
        WHERE 1=1 ${campaignFilter} ${dateGteFilter} ${dateLteFilter}
      `,
      prisma.representativeResponse.count({
        where: { ...responseWhere, status: ResponseStatus.APPROVED },
      }),
      prisma.representativeResponse.count({
        where: { ...responseWhere, isVerified: true },
      }),
      prisma.call.count({ where: callWhere }),
    ]);

    const participantCount = Number(uniqueParticipants[0]?.count || 0);

    const stages = [
      { name: 'Emails Sent', count: emailsSent },
      { name: 'Unique Participants', count: participantCount },
      { name: 'Responses Received', count: approvedResponses },
      { name: 'Verified Responses', count: verifiedResponses },
      { name: 'Calls Made', count: callsMade },
    ];

    // Compute percentages relative to first stage and dropoff from previous
    const firstCount = stages[0].count || 1;
    return stages.map((stage, i) => ({
      ...stage,
      percentOfFirst: stage.count / firstCount,
      dropoff: i > 0
        ? (stages[i - 1].count > 0
          ? 1 - stage.count / stages[i - 1].count
          : 0)
        : 0,
    }));
  },

  /**
   * Time-series: daily/weekly email + response volumes
   */
  async getActivityTrends(query: TrendQuery) {
    const dateFilter = buildDateFilter(query);
    const truncFn = query.granularity === 'week' ? 'week' : 'day';

    // Default: last 30 days
    const defaultFrom = new Date();
    defaultFrom.setDate(defaultFrom.getDate() - 30);

    const from = dateFilter?.gte || defaultFrom;
    const to = dateFilter?.lte || new Date();

    // Use a lookup map instead of Prisma.raw() to prevent SQL injection if enum changes
    const truncFnMap: Record<string, ReturnType<typeof Prisma.sql>> = {
      day: Prisma.sql`'day'`,
      week: Prisma.sql`'week'`,
    };
    const truncFnSql = truncFnMap[truncFn] || truncFnMap.day;
    const campaignFilter = query.campaignId
      ? Prisma.sql`AND "campaignId" = ${query.campaignId}`
      : Prisma.sql``;

    const [emailTrends, responseTrends] = await Promise.all([
      prisma.$queryRaw<Array<{ period: Date; count: bigint }>>`
        SELECT DATE_TRUNC(${truncFnSql}, "sentAt") as period, COUNT(*) as count
        FROM campaign_emails
        WHERE "sentAt" >= ${from} AND "sentAt" <= ${to}
          ${campaignFilter}
        GROUP BY period
        ORDER BY period ASC
      `,
      prisma.$queryRaw<Array<{ period: Date; count: bigint }>>`
        SELECT DATE_TRUNC(${truncFnSql}, "createdAt") as period, COUNT(*) as count
        FROM representative_responses
        WHERE "createdAt" >= ${from} AND "createdAt" <= ${to}
          AND status = 'APPROVED'
          ${campaignFilter}
        GROUP BY period
        ORDER BY period ASC
      `,
    ]);

    // Merge into a single series with both email and response counts
    const periodMap = new Map<string, { emails: number; responses: number }>();

    for (const row of emailTrends) {
      const key = row.period.toISOString().split('T')[0];
      if (!periodMap.has(key)) periodMap.set(key, { emails: 0, responses: 0 });
      periodMap.get(key)!.emails = Number(row.count);
    }

    for (const row of responseTrends) {
      const key = row.period.toISOString().split('T')[0];
      if (!periodMap.has(key)) periodMap.set(key, { emails: 0, responses: 0 });
      periodMap.get(key)!.responses = Number(row.count);
    }

    // Sort by date and return
    const series = Array.from(periodMap.entries())
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([date, counts]) => ({
        date,
        emails: counts.emails,
        responses: counts.responses,
      }));

    return {
      granularity: query.granularity,
      dateFrom: from.toISOString(),
      dateTo: to.toISOString(),
      series,
    };
  },
};
