import { Prisma, SupportLevel, VisitOutcome } from '@prisma/client';
import { prisma } from '../../../config/database';
import { isPointInPolygon, parseGeoJsonPolygon, calculateBounds } from '../../../utils/spatial';
import { logger } from '../../../utils/logger';
import type { ExportContactsPreviewInput, ExportContactsInput } from './canvass-export.schemas';

const SUPPORT_LEVEL_ORDER: Record<string, number> = {
  LEVEL_1: 1,
  LEVEL_2: 2,
  LEVEL_3: 3,
  LEVEL_4: 4,
};

const MAX_EXPORT = 10_000;

// --- Types ---

interface ContactCandidate {
  addressId: string;
  firstName: string | null;
  lastName: string | null;
  email: string | null;
  supportLevel: SupportLevel | null;
  sign: boolean;
  cutName: string;
  locationAddress: string;
  unitNumber: string | null;
  latestOutcome: VisitOutcome | null;
  latestVisitDate: Date | null;
}

export interface ExportPreviewResult {
  totalContacts: number;
  contactsWithEmail: number;
  byCut: { cutId: string; cutName: string; contacts: number; withEmail: number }[];
  byOutcome: Record<string, number>;
  bySupportLevel: Record<string, number>;
}

export interface ExportResult {
  created: number;
  skippedDuplicate: number;
  skippedNoEmail: number;
  campaignId: string;
  campaignTitle: string;
}

export interface CutCampaignAnalytics {
  cutId: string;
  cutName: string;
  totalAddresses: number;
  visitedAddresses: number;
  completionPct: number;
  addressesWithEmail: number;
  supportBreakdown: Record<string, number>;
}

// --- Helpers ---

async function resolveContactsFromCuts(
  filters: ExportContactsPreviewInput,
): Promise<ContactCandidate[]> {
  const cuts = await prisma.cut.findMany({
    where: { id: { in: filters.cutIds } },
    select: { id: true, name: true, geojson: true },
  });

  const allContacts: ContactCandidate[] = [];

  for (const cut of cuts) {
    const polygons = parseGeoJsonPolygon(cut.geojson);

    // Calculate bounds for a fast DB pre-filter
    const allCoords = polygons.flat();
    const bounds = calculateBounds(allCoords);

    const locations = await prisma.location.findMany({
      where: {
        latitude: { gte: new Prisma.Decimal(bounds.minLat.toString()), lte: new Prisma.Decimal(bounds.maxLat.toString()) },
        longitude: { gte: new Prisma.Decimal(bounds.minLng.toString()), lte: new Prisma.Decimal(bounds.maxLng.toString()) },
      },
      select: {
        id: true,
        latitude: true,
        longitude: true,
        address: true,
        addresses: {
          select: {
            id: true,
            unitNumber: true,
            firstName: true,
            lastName: true,
            email: true,
            supportLevel: true,
            sign: true,
          },
        },
      },
    });

    // In-memory polygon filter
    for (const loc of locations) {
      const lat = Number(loc.latitude);
      const lng = Number(loc.longitude);
      if (!polygons.some(p => isPointInPolygon(lat, lng, p))) continue;

      for (const addr of loc.addresses) {
        allContacts.push({
          addressId: addr.id,
          firstName: addr.firstName,
          lastName: addr.lastName,
          email: addr.email,
          supportLevel: addr.supportLevel,
          sign: addr.sign,
          cutName: cut.name,
          locationAddress: loc.address,
          unitNumber: addr.unitNumber,
          latestOutcome: null,
          latestVisitDate: null,
        });
      }
    }
  }

  if (allContacts.length === 0) return [];

  // Fetch latest visit for each address to apply outcome/date filters
  const addressIds = [...new Set(allContacts.map(c => c.addressId))];
  const latestVisits = await prisma.canvassVisit.findMany({
    where: { addressId: { in: addressIds } },
    distinct: ['addressId'],
    orderBy: { visitedAt: 'desc' },
    select: { addressId: true, outcome: true, visitedAt: true },
  });
  const visitMap = new Map(latestVisits.map(v => [v.addressId, v]));

  // Annotate contacts with latest visit data
  for (const contact of allContacts) {
    const visit = visitMap.get(contact.addressId);
    if (visit) {
      contact.latestOutcome = visit.outcome;
      contact.latestVisitDate = visit.visitedAt;
    }
  }

  // Apply filters
  let filtered = allContacts;

  if (filters.outcomes && filters.outcomes.length > 0) {
    const outcomeSet = new Set(filters.outcomes);
    filtered = filtered.filter(c => c.latestOutcome && outcomeSet.has(c.latestOutcome));
  }

  if (filters.supportLevelMin !== undefined || filters.supportLevelMax !== undefined) {
    const min = filters.supportLevelMin ?? 1;
    const max = filters.supportLevelMax ?? 4;
    filtered = filtered.filter(c => {
      if (!c.supportLevel) return false;
      const level = SUPPORT_LEVEL_ORDER[c.supportLevel] ?? 0;
      return level >= min && level <= max;
    });
  }

  if (filters.hasEmail === true) {
    filtered = filtered.filter(c => !!c.email);
  }

  if (filters.hasSign === true) {
    filtered = filtered.filter(c => c.sign);
  }

  if (filters.visitedSince) {
    const since = new Date(filters.visitedSince);
    filtered = filtered.filter(c => c.latestVisitDate && c.latestVisitDate >= since);
  }

  // Deduplicate by addressId (in case of overlapping cuts)
  const seen = new Set<string>();
  const deduped: ContactCandidate[] = [];
  for (const c of filtered) {
    if (!seen.has(c.addressId)) {
      seen.add(c.addressId);
      deduped.push(c);
    }
  }

  return deduped.slice(0, MAX_EXPORT);
}

// --- Service Functions ---

export async function previewExportContacts(
  filters: ExportContactsPreviewInput,
): Promise<ExportPreviewResult> {
  const contacts = await resolveContactsFromCuts(filters);

  const byCut: Record<string, { cutName: string; contacts: number; withEmail: number }> = {};
  const byOutcome: Record<string, number> = {};
  const bySupportLevel: Record<string, number> = {};
  let withEmail = 0;

  for (const c of contacts) {
    // By cut
    const cutKey = c.cutName;
    if (!byCut[cutKey]) byCut[cutKey] = { cutName: cutKey, contacts: 0, withEmail: 0 };
    byCut[cutKey].contacts++;
    if (c.email) {
      byCut[cutKey].withEmail++;
      withEmail++;
    }

    // By outcome
    if (c.latestOutcome) {
      byOutcome[c.latestOutcome] = (byOutcome[c.latestOutcome] || 0) + 1;
    }

    // By support level
    if (c.supportLevel) {
      bySupportLevel[c.supportLevel] = (bySupportLevel[c.supportLevel] || 0) + 1;
    }
  }

  // Map byCut to array with cutIds
  const cuts = await prisma.cut.findMany({
    where: { id: { in: filters.cutIds } },
    select: { id: true, name: true },
  });
  const cutIdMap = new Map(cuts.map(c => [c.name, c.id]));

  return {
    totalContacts: contacts.length,
    contactsWithEmail: withEmail,
    byCut: Object.entries(byCut).map(([name, data]) => ({
      cutId: cutIdMap.get(name) || '',
      cutName: name,
      contacts: data.contacts,
      withEmail: data.withEmail,
    })),
    byOutcome,
    bySupportLevel,
  };
}

export async function exportContactsToCampaign(
  filters: ExportContactsInput,
): Promise<ExportResult> {
  const campaign = await prisma.campaign.findUnique({
    where: { id: filters.campaignId },
    select: { id: true, title: true, slug: true, allowCustomRecipients: true },
  });

  if (!campaign) {
    throw new Error('Campaign not found');
  }

  // Auto-enable custom recipients if not already
  if (!campaign.allowCustomRecipients) {
    await prisma.campaign.update({
      where: { id: campaign.id },
      data: { allowCustomRecipients: true },
    });
  }

  const contacts = await resolveContactsFromCuts(filters);

  // Get existing custom recipients to deduplicate
  const existing = await prisma.customRecipient.findMany({
    where: { campaignId: campaign.id },
    select: { recipientEmail: true },
  });
  const existingEmails = new Set(existing.map(r => r.recipientEmail.toLowerCase()));

  let created = 0;
  let skippedDuplicate = 0;
  let skippedNoEmail = 0;

  // Deduplicate by email across all contacts
  const emailsSeen = new Set<string>();

  const toCreate: Prisma.CustomRecipientCreateManyInput[] = [];

  for (const c of contacts) {
    if (!c.email) {
      skippedNoEmail++;
      continue;
    }

    const emailLower = c.email.toLowerCase();

    if (existingEmails.has(emailLower) || emailsSeen.has(emailLower)) {
      skippedDuplicate++;
      continue;
    }

    emailsSeen.add(emailLower);

    const name = [c.firstName, c.lastName].filter(Boolean).join(' ') || c.email;
    const addrLabel = c.unitNumber
      ? `${c.locationAddress} Unit ${c.unitNumber}`
      : c.locationAddress;

    toCreate.push({
      campaignId: campaign.id,
      campaignSlug: campaign.slug,
      recipientName: name,
      recipientEmail: c.email,
      notes: `From canvass: ${c.cutName} — ${addrLabel}${c.supportLevel ? ` (${c.supportLevel})` : ''}${c.latestOutcome ? ` [${c.latestOutcome}]` : ''}`,
    });
  }

  if (toCreate.length > 0) {
    await prisma.customRecipient.createMany({ data: toCreate });
    created = toCreate.length;
  }

  logger.info(`Exported ${created} contacts to campaign "${campaign.title}" (${skippedDuplicate} dupes, ${skippedNoEmail} no email)`);

  return {
    created,
    skippedDuplicate,
    skippedNoEmail,
    campaignId: campaign.id,
    campaignTitle: campaign.title,
  };
}

export async function getCutCampaignAnalytics(): Promise<CutCampaignAnalytics[]> {
  const cuts = await prisma.cut.findMany({
    select: { id: true, name: true, geojson: true },
    orderBy: { name: 'asc' },
  });

  const results: CutCampaignAnalytics[] = [];

  for (const cut of cuts) {
    const polygons = parseGeoJsonPolygon(cut.geojson);
    const allCoords = polygons.flat();
    const bounds = calculateBounds(allCoords);

    const locations = await prisma.location.findMany({
      where: {
        latitude: { gte: new Prisma.Decimal(bounds.minLat.toString()), lte: new Prisma.Decimal(bounds.maxLat.toString()) },
        longitude: { gte: new Prisma.Decimal(bounds.minLng.toString()), lte: new Prisma.Decimal(bounds.maxLng.toString()) },
      },
      select: {
        latitude: true,
        longitude: true,
        addresses: {
          select: { id: true, email: true, supportLevel: true },
        },
      },
    });

    let totalAddresses = 0;
    let addressesWithEmail = 0;
    const addressIds: string[] = [];
    const supportBreakdown: Record<string, number> = {};

    for (const loc of locations) {
      if (!polygons.some(p => isPointInPolygon(Number(loc.latitude), Number(loc.longitude), p))) continue;

      for (const addr of loc.addresses) {
        totalAddresses++;
        addressIds.push(addr.id);
        if (addr.email) addressesWithEmail++;
        if (addr.supportLevel) {
          supportBreakdown[addr.supportLevel] = (supportBreakdown[addr.supportLevel] || 0) + 1;
        }
      }
    }

    const visitedCount = addressIds.length > 0
      ? await prisma.canvassVisit.findMany({
          where: { addressId: { in: addressIds } },
          distinct: ['addressId'],
          select: { addressId: true },
        }).then(r => r.length)
      : 0;

    results.push({
      cutId: cut.id,
      cutName: cut.name,
      totalAddresses,
      visitedAddresses: visitedCount,
      completionPct: totalAddresses > 0 ? Math.round((visitedCount / totalAddresses) * 100) : 0,
      addressesWithEmail,
      supportBreakdown,
    });
  }

  return results;
}
