import { Prisma, CanvassSessionStatus, VisitOutcome, SignupStatus, UserRole } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
import { prisma } from '../../../config/database';
import { AppError } from '../../../middleware/error-handler';
import { logger } from '../../../utils/logger';
import { recordLocationQuery } from '../../../utils/metrics';
import { isPointInPolygon, parseGeoJsonPolygon } from '../../../utils/spatial';
import { recordCrmActivity } from '../../../utils/crm-activity';
import { calculateWalkingRoute } from './canvass-route.service';
import { recordCanvassVisit, setActiveCanvassSessions } from '../../../utils/metrics';
import { notificationQueueService } from '../../../services/notification-queue.service';
import { getAdminEmailsByRole, isNotificationEnabled } from '../../../services/notification.helper';
import { env } from '../../../config/env';
import { eventBus } from '../../../services/event-bus.service';
import { achievementsService } from '../../social/achievements.service';
import type {
  RecordVisitInput,
  BulkRecordVisitInput,
  StartSessionInput,
  WalkingRouteInput,
  ListMyVisitsInput,
  AdminActivityInput,
  AdminVisitsInput,
  VolunteerUpdateLocationInput,
  OutcomeTrendsQueryInput,
} from './canvass.schemas';

const ADDRESS_SELECT = {
  id: true,
  unitNumber: true,
  firstName: true,
  lastName: true,
  email: true,
  phone: true,
  supportLevel: true,
  sign: true,
  signSize: true,
  notes: true,
  location: {
    select: {
      id: true,
      latitude: true,
      longitude: true,
      address: true,
      buildingNotes: true,
    },
  },
} as const;

type AddressForCanvass = Prisma.AddressGetPayload<{ select: typeof ADDRESS_SELECT }>;

async function annotateAddressesWithVisits(
  addresses: AddressForCanvass[],
  userId: string,
) {
  const addressIds = addresses.map((a) => a.id);
  if (addressIds.length === 0) return [];

  const latestVisits = await prisma.canvassVisit.findMany({
    where: { addressId: { in: addressIds } },
    orderBy: { visitedAt: 'desc' },
    distinct: ['addressId'],
    select: {
      addressId: true,
      outcome: true,
      visitedAt: true,
      userId: true,
      user: { select: { name: true } },
    },
  });

  const visitMap = new Map(latestVisits.map((v) => [v.addressId, v]));

  return addresses.map((addr) => {
    const visit = visitMap.get(addr.id);
    return {
      ...addr,
      location: {
        ...addr.location,
        latitude: Number(addr.location.latitude),
        longitude: Number(addr.location.longitude),
      },
      lastVisit: visit
        ? {
            outcome: visit.outcome,
            visitedAt: visit.visitedAt,
            visitorName: visit.user?.name ?? null,
            isMyVisit: visit.userId === userId,
          }
        : null,
    };
  });
}

const ADMIN_ADDRESS_FIELDS = ['firstName', 'lastName', 'unitNumber', 'email', 'phone'] as const;
const VOLUNTEER_ADDRESS_FIELDS = ['supportLevel', 'sign', 'signSize', 'notes'] as const;

/** Strip PII fields from address data for non-admin volunteer access */
function stripAddressPii(addresses: any[]): any[] {
  return addresses.map(({ firstName, lastName, email, phone, ...safe }) => ({
    ...safe,
    // Volunteers see support data but not personal contact info
    hasContactInfo: !!(firstName || lastName || email || phone),
  }));
}

export const canvassService = {
  // ─── Volunteer Methods ─────────────────────────────────────────────

  async getMyAssignments(userId: string) {
    const signups = await prisma.shiftSignup.findMany({
      where: {
        userId,
        status: SignupStatus.CONFIRMED,
        shift: { cutId: { not: null } },
      },
      include: {
        shift: {
          include: {
            cut: { select: { id: true, name: true, completionPercentage: true, geojson: true } },
          },
        },
      },
      orderBy: { shift: { date: 'asc' } },
    });

    return signups
      .filter((s) => s.shift.cut)
      .map((s) => ({
        shiftId: s.shift.id,
        shiftTitle: s.shift.title,
        shiftDate: s.shift.date,
        startTime: s.shift.startTime,
        endTime: s.shift.endTime,
        location: s.shift.location,
        cutId: s.shift.cut!.id,
        cutName: s.shift.cut!.name,
        completionPercentage: s.shift.cut!.completionPercentage,
      }));
  },

  async getMyStats(userId: string) {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const [totalVisits, todayVisits, byOutcome, sessions] = await Promise.all([
      prisma.canvassVisit.count({ where: { userId } }),
      prisma.canvassVisit.count({ where: { userId, visitedAt: { gte: today } } }),
      prisma.canvassVisit.groupBy({
        by: ['outcome'],
        where: { userId },
        _count: true,
      }),
      prisma.canvassSession.count({ where: { userId } }),
    ]);

    const outcomeMap: Record<string, number> = {};
    for (const row of byOutcome) {
      outcomeMap[row.outcome] = row._count;
    }

    return { totalVisits, todayVisits, byOutcome: outcomeMap, sessions };
  },

  async getMyVisits(userId: string, filters: ListMyVisitsInput) {
    const { page, limit } = filters;
    const skip = (page - 1) * limit;

    const [visits, total] = await Promise.all([
      prisma.canvassVisit.findMany({
        where: { userId },
        skip,
        take: limit,
        orderBy: { visitedAt: 'desc' },
        include: {
          address: {
            select: {
              id: true,
              unitNumber: true,
              location: { select: { address: true } },
            },
          },
        },
      }),
      prisma.canvassVisit.count({ where: { userId } }),
    ]);

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

  async getActiveSession(userId: string) {
    return prisma.canvassSession.findFirst({
      where: { userId, status: CanvassSessionStatus.ACTIVE },
      include: {
        cut: { select: { id: true, name: true } },
        shift: { select: { id: true, title: true } },
      },
    });
  },

  async startSession(userId: string, data: StartSessionInput) {
    // Check for existing active session
    const existing = await prisma.canvassSession.findFirst({
      where: { userId, status: CanvassSessionStatus.ACTIVE },
    });
    if (existing) {
      throw new AppError(409, 'You already have an active canvass session', 'SESSION_ACTIVE');
    }

    // Verify cut exists
    const cut = await prisma.cut.findUnique({ where: { id: data.cutId } });
    if (!cut) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }

    const session = await prisma.canvassSession.create({
      data: {
        userId,
        cutId: data.cutId,
        shiftId: data.shiftId,
        startLatitude: data.startLatitude,
        startLongitude: data.startLongitude,
      },
      include: {
        cut: { select: { id: true, name: true } },
        shift: { select: { id: true, title: true } },
      },
    });

    return session;
  },

  async endSession(sessionId: string, userId: string) {
    const session = await prisma.canvassSession.findUnique({ where: { id: sessionId } });
    if (!session) {
      throw new AppError(404, 'Session not found', 'SESSION_NOT_FOUND');
    }
    if (session.userId !== userId) {
      throw new AppError(403, 'Not your session', 'FORBIDDEN');
    }
    if (session.status !== CanvassSessionStatus.ACTIVE) {
      throw new AppError(400, 'Session is not active', 'SESSION_NOT_ACTIVE');
    }

    const updated = await prisma.canvassSession.update({
      where: { id: sessionId },
      data: { status: CanvassSessionStatus.COMPLETED, endedAt: new Date() },
    });

    // Recalculate cut completion percentage
    await this.recalculateCutCompletion(session.cutId);

    // Notification: volunteer session summary
    try {
      if (await isNotificationEnabled('notifyVolunteerSessionSummary')) {
        const [user, cut, visitCount, outcomeGroups, trackingSession] = await Promise.all([
          prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } }),
          prisma.cut.findUnique({ where: { id: session.cutId }, select: { name: true } }),
          prisma.canvassVisit.count({ where: { sessionId } }),
          prisma.canvassVisit.groupBy({ by: ['outcome'], where: { sessionId }, _count: true }),
          prisma.trackingSession.findFirst({
            where: { userId, canvassSessionId: sessionId },
            select: { totalDistanceM: true },
          }),
        ]);

        if (user && visitCount > 0) {
          const durationMs = updated.endedAt
            ? updated.endedAt.getTime() - session.startedAt.getTime()
            : 0;
          const durationMinutes = Math.round(durationMs / 60000);
          const distanceKm = trackingSession?.totalDistanceM
            ? Number(trackingSession.totalDistanceM) / 1000
            : 0;

          const outcomeBreakdown: Record<string, number> = {};
          for (const row of outcomeGroups) {
            outcomeBreakdown[row.outcome] = row._count;
          }

          await notificationQueueService.enqueue({
            type: 'volunteer-session-summary',
            volunteerEmail: user.email,
            volunteerName: user.name || user.email,
            cutName: cut?.name || 'Unknown area',
            sessionDate: session.startedAt.toLocaleDateString('en-CA', {
              weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
            }),
            visitCount,
            durationMinutes,
            distanceKm,
            outcomeBreakdown,
          });
        }
      }
    } catch (err) {
      logger.error('Failed to enqueue session summary notification:', err);
    }

    // Publish canvass session completed event (consumed by RC, Listmonk, etc.)
    try {
      const [syncUser, syncCut, syncVisitCount, syncOutcomes] = await Promise.all([
        prisma.user.findUnique({ where: { id: userId }, select: { email: true, name: true } }),
        prisma.cut.findUnique({ where: { id: session.cutId }, select: { name: true } }),
        prisma.canvassVisit.count({ where: { sessionId } }),
        prisma.canvassVisit.groupBy({ by: ['outcome'], where: { sessionId }, _count: true }),
      ]);
      if (syncUser) {
        const outcomes: Record<string, number> = {};
        for (const row of syncOutcomes) {
          outcomes[row.outcome] = row._count;
        }
        eventBus.publish('canvass.session.completed', {
          sessionId,
          userId,
          userName: syncUser.name || syncUser.email,
          userEmail: syncUser.email,
          cutName: syncCut?.name || 'Unknown',
          visitCount: syncVisitCount,
          outcomes,
        });
      }
    } catch { /* non-critical */ }

    return updated;
  },

  async getCutLocationsForCanvass(
    cutId: string,
    userId: string,
    bounds?: { minLat: number; maxLat: number; minLng: number; maxLng: number },
    limit?: number,
  ) {
    const startTime = Date.now();
    const cut = await prisma.cut.findUnique({ where: { id: cutId } });
    if (!cut) {
      throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');
    }

    const polygons = parseGeoJsonPolygon(cut.geojson);

    // Two-stage filtering: bounds first (fast DB query), then polygon (in-memory)
    const where: Prisma.LocationWhereInput = {};

    if (bounds) {
      // Convert to Decimal for proper PostgreSQL type matching
      where.latitude = {
        gte: new Decimal(bounds.minLat.toString()),
        lte: new Decimal(bounds.maxLat.toString())
      };
      where.longitude = {
        gte: new Decimal(bounds.minLng.toString()),
        lte: new Decimal(bounds.maxLng.toString())
      };
    }

    // CRITICAL: Apply limit to ADDRESSES not locations
    // Fetch more locations than needed to account for multi-unit buildings
    const addressLimit = Math.min(limit || 5000, 5000);
    const locationFetchLimit = Math.ceil(addressLimit * 1.5); // Fetch 50% more locations to ensure we get enough addresses

    const allLocations = await prisma.location.findMany({
      where,
      select: {
        id: true,
        latitude: true,
        longitude: true,
        address: true,
        buildingNotes: true,
        addresses: {
          select: {
            id: true,
            unitNumber: true,
            firstName: true,
            lastName: true,
            email: true,
            phone: true,
            supportLevel: true,
            sign: true,
            signSize: true,
            notes: true,
          },
        },
      },
      take: locationFetchLimit,
    });

    // Filter locations by polygon and flatten addresses, RESPECTING ADDRESS LIMIT
    const addressesInCut: AddressForCanvass[] = [];
    let addressCount = 0;

    for (const loc of allLocations) {
      // Stop if we've reached the address limit
      if (addressCount >= addressLimit) {
        break;
      }

      const lat = Number(loc.latitude);
      const lng = Number(loc.longitude);

      if (polygons.some((poly) => isPointInPolygon(lat, lng, poly))) {
        for (const addr of loc.addresses) {
          // Check limit before adding each address
          if (addressCount >= addressLimit) {
            break;
          }

          addressesInCut.push({
            ...addr,
            location: {
              id: loc.id,
              latitude: loc.latitude,
              longitude: loc.longitude,
              address: loc.address,
              buildingNotes: loc.buildingNotes,
            },
          });
          addressCount++;
        }
      }
    }

    const result = await annotateAddressesWithVisits(addressesInCut, userId);

    const durationSeconds = (Date.now() - startTime) / 1000;
    recordLocationQuery('canvass_cut', !!bounds, result.length, durationSeconds);

    // Strip PII for volunteer access (firstName, lastName, email, phone)
    return stripAddressPii(result);
  },

  async getAllLocationsForCanvass(
    userId: string,
    bounds?: { minLat: number; maxLat: number; minLng: number; maxLng: number },
    limit?: number,
  ) {
    const startTime = Date.now();
    const where: Prisma.LocationWhereInput = {};

    if (bounds) {
      // Convert to Decimal for proper PostgreSQL type matching
      where.latitude = {
        gte: new Decimal(bounds.minLat.toString()),
        lte: new Decimal(bounds.maxLat.toString())
      };
      where.longitude = {
        gte: new Decimal(bounds.minLng.toString()),
        lte: new Decimal(bounds.maxLng.toString())
      };
    }

    // CRITICAL: Apply limit to ADDRESSES not locations
    // Fetch more locations than needed to account for multi-unit buildings
    const addressLimit = Math.min(limit || 5000, 5000);
    const locationFetchLimit = Math.ceil(addressLimit * 1.5); // Fetch 50% more locations to ensure we get enough addresses

    const allLocations = await prisma.location.findMany({
      where,
      select: {
        id: true,
        latitude: true,
        longitude: true,
        address: true,
        buildingNotes: true,
        addresses: {
          select: {
            id: true,
            unitNumber: true,
            firstName: true,
            lastName: true,
            email: true,
            phone: true,
            supportLevel: true,
            sign: true,
            signSize: true,
            notes: true,
          },
        },
      },
      take: locationFetchLimit,
    });

    // Flatten addresses with location embedded, RESPECTING ADDRESS LIMIT
    const allAddresses: AddressForCanvass[] = [];
    let addressCount = 0;

    for (const loc of allLocations) {
      // Stop if we've reached the address limit
      if (addressCount >= addressLimit) {
        break;
      }

      for (const addr of loc.addresses) {
        // Check limit before adding each address
        if (addressCount >= addressLimit) {
          break;
        }

        allAddresses.push({
          ...addr,
          location: {
            id: loc.id,
            latitude: loc.latitude,
            longitude: loc.longitude,
            address: loc.address,
            buildingNotes: loc.buildingNotes,
          },
        });
        addressCount++;
      }
    }

    const result = await annotateAddressesWithVisits(allAddresses, userId);

    const durationSeconds = (Date.now() - startTime) / 1000;
    recordLocationQuery('canvass_all', !!bounds, result.length, durationSeconds);

    // Strip PII for volunteer access (firstName, lastName, email, phone)
    return stripAddressPii(result);
  },

  async updateAddressAsVolunteer(
    addressId: string,
    userId: string,
    role: UserRole,
    data: VolunteerUpdateLocationInput,
  ) {
    const existing = await prisma.address.findUnique({ where: { id: addressId } });
    if (!existing) {
      throw new AppError(404, 'Address not found', 'ADDRESS_NOT_FOUND');
    }

    const isAdmin = role === UserRole.SUPER_ADMIN || role === UserRole.MAP_ADMIN;

    // Build update data, stripping admin-only fields for non-admins
    const updateData: Prisma.AddressUncheckedUpdateInput = {
      updatedByUserId: userId,
    };

    for (const field of VOLUNTEER_ADDRESS_FIELDS) {
      if (data[field] !== undefined) {
        (updateData as Record<string, unknown>)[field] = data[field];
      }
    }

    if (isAdmin) {
      for (const field of ADMIN_ADDRESS_FIELDS) {
        if (data[field] !== undefined) {
          (updateData as Record<string, unknown>)[field] = data[field];
        }
      }
    }

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

  async getWalkingRoute(cutId: string, userId: string, filters: WalkingRouteInput) {
    const addresses = await this.getCutLocationsForCanvass(cutId, userId);

    let filtered = addresses;
    if (filters.excludeVisited) {
      filtered = addresses.filter((a) => !a.lastVisit);
    }

    const routeLocations = filtered.map((a) => ({
      id: a.location.id, // Use location ID for routing
      latitude: a.location.latitude,
      longitude: a.location.longitude,
    }));

    const cut = await prisma.cut.findUnique({
      where: { id: cutId },
      select: { geojson: true },
    });

    return calculateWalkingRoute(
      routeLocations,
      filters.startLatitude,
      filters.startLongitude,
      cut?.geojson,
    );
  },

  async recordVisit(userId: string, data: RecordVisitInput) {
    // Verify address exists
    const address = await prisma.address.findUnique({ where: { id: data.addressId } });
    if (!address) {
      throw new AppError(404, 'Address not found', 'ADDRESS_NOT_FOUND');
    }

    // Create visit record
    const visit = await prisma.canvassVisit.create({
      data: {
        addressId: data.addressId,
        userId,
        shiftId: data.shiftId,
        sessionId: data.sessionId,
        outcome: data.outcome,
        supportLevel: data.supportLevel,
        signRequested: data.signRequested ?? false,
        signSize: data.signSize,
        notes: data.notes,
        durationSeconds: data.durationSeconds,
      },
      include: {
        address: {
          select: {
            id: true,
            unitNumber: true,
            location: { select: { address: true } },
          },
        },
      },
    });

    // If SPOKE_WITH and updateLocation, push data to the address record
    if (data.outcome === VisitOutcome.SPOKE_WITH && data.updateLocation) {
      const updateData: Prisma.AddressUncheckedUpdateInput = {
        updatedByUserId: userId,
      };

      if (data.supportLevel) updateData.supportLevel = data.supportLevel;
      if (data.signRequested !== undefined) updateData.sign = data.signRequested;
      if (data.signSize) updateData.signSize = data.signSize;
      if (data.notes) {
        const timestamp = new Date().toISOString().split('T')[0];
        const prefix = `[${timestamp}] ${data.notes}`;
        updateData.notes = address.notes ? `${prefix}\n${address.notes}` : prefix;
      }

      const updatedAddress = await prisma.address.update({
        where: { id: data.addressId },
        data: updateData,
        include: { location: { select: { address: true } } },
      });

      // Publish address updated event (consumed by Listmonk, etc.)
      if (updatedAddress.email) {
        const name = [updatedAddress.firstName, updatedAddress.lastName].filter(Boolean).join(' ');
        eventBus.publish('contact.address.updated', {
          email: updatedAddress.email,
          name,
          supportLevel: updatedAddress.supportLevel,
          sign: updatedAddress.sign,
          address: updatedAddress.location.address,
        });
      }
    }

    recordCanvassVisit(data.outcome);

    // CRM activity via ContactAddress lookup (fire-and-forget)
    prisma.contactAddress.findFirst({
      where: { addressId: data.addressId },
      select: { contactId: true },
    }).then((ca) => {
      if (ca) {
        recordCrmActivity({
          contactId: ca.contactId,
          activityType: 'CANVASS_VISIT',
          title: `Canvass visit: ${data.outcome}`,
          metadata: { addressId: data.addressId, outcome: data.outcome, visitId: visit.id },
        }).catch(() => {});
      }
    }).catch(() => {});

    // Achievement check (fire-and-forget)
    achievementsService.checkAndUnlock(userId, ['canvass']).catch(() => {});

    // Notification: sign request alert for admins
    if (data.signRequested) {
      try {
        if (await isNotificationEnabled('notifyAdminSignRequested')) {
          const adminEmails = await getAdminEmailsByRole([UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN]);
          if (adminEmails.length > 0) {
            const [volunteer, shift] = await Promise.all([
              prisma.user.findUnique({ where: { id: userId }, select: { name: true } }),
              data.shiftId ? prisma.shift.findUnique({ where: { id: data.shiftId }, select: { title: true } }) : null,
            ]);

            const addressStr = visit.address?.location?.address || 'Unknown address';
            const adminUrl = `${env.ADMIN_URL || 'http://localhost:3000'}/app/canvass/dashboard`;

            await notificationQueueService.enqueue({
              type: 'admin-sign-requested',
              adminEmails,
              volunteerName: volunteer?.name || 'Unknown',
              address: addressStr,
              shiftTitle: shift?.title || 'No shift',
              signSize: data.signSize || '',
              adminUrl,
            });
          }
        }
      } catch (err) {
        logger.error('Failed to enqueue sign request notification:', err);
      }
    }

    return visit;
  },

  async recordBulkVisit(userId: string, data: BulkRecordVisitInput) {
    // Verify session exists and belongs to user
    if (data.sessionId) {
      const session = await prisma.canvassSession.findUnique({
        where: { id: data.sessionId },
        include: { cut: true },
      });

      if (!session || session.userId !== userId) {
        throw new AppError(403, 'Invalid session', 'INVALID_SESSION');
      }

      if (session.status !== 'ACTIVE') {
        throw new AppError(400, 'Session is not active', 'INACTIVE_SESSION');
      }

      // Verify location is within cut boundary
      if (session.cutId && session.cut) {
        const location = await prisma.location.findUnique({
          where: { id: data.locationId },
        });

        if (!location) {
          throw new AppError(404, 'Location not found', 'LOCATION_NOT_FOUND');
        }

        if (session.cut.geojson) {
          try {
            const polygons = parseGeoJsonPolygon(session.cut.geojson);
            const isWithinBoundary = polygons.some((polygon) =>
              isPointInPolygon(
                Number(location.latitude),
                Number(location.longitude),
                polygon
              )
            );

            if (!isWithinBoundary) {
              throw new AppError(403, 'Location is outside your assigned territory', 'LOCATION_OUT_OF_BOUNDS');
            }
          } catch (err) {
            logger.error('Failed to validate location boundary for bulk visit', { error: err instanceof Error ? err.message : JSON.stringify(err) });
            // Don't block the visit if polygon parsing fails, but log it
          }
        }
      }
    }

    // Get all unvisited addresses at this location
    const addresses = await prisma.address.findMany({
      where: {
        locationId: data.locationId,
        canvassVisits: { none: {} },
      },
    });

    if (addresses.length === 0) {
      throw new AppError(400, 'No unvisited addresses found at this location', 'NO_UNVISITED_ADDRESSES');
    }

    // Create visit record for each address
    const visits = await Promise.all(
      addresses.map((addr) =>
        prisma.canvassVisit.create({
          data: {
            addressId: addr.id,
            userId,
            sessionId: data.sessionId,
            shiftId: data.shiftId,
            outcome: data.outcome,
            notes: data.notes ? `[BULK] ${data.notes}` : null,
            visitedAt: new Date(),
          },
        })
      )
    );

    visits.forEach(() => recordCanvassVisit(data.outcome));

    return { created: visits.length, visits };
  },

  // ─── Admin Methods ─────────────────────────────────────────────────

  async getAdminStats() {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const [totalVisits, todayVisits, activeSessions, activeVolunteers, cuts] = await Promise.all([
      prisma.canvassVisit.count(),
      prisma.canvassVisit.count({ where: { visitedAt: { gte: today } } }),
      prisma.canvassSession.count({ where: { status: CanvassSessionStatus.ACTIVE } }),
      prisma.canvassVisit.findMany({
        where: { visitedAt: { gte: today } },
        distinct: ['userId'],
        select: { userId: true },
      }),
      prisma.cut.findMany({
        select: { id: true, name: true, completionPercentage: true },
      }),
    ]);

    setActiveCanvassSessions(activeSessions);

    const avgCompletion = cuts.length > 0
      ? Math.round(cuts.reduce((sum, c) => sum + c.completionPercentage, 0) / cuts.length)
      : 0;

    return {
      totalVisits,
      todayVisits,
      activeSessions,
      activeVolunteers: activeVolunteers.length,
      overallCompletion: avgCompletion,
    };
  },

  async getCutStats(cutId: string) {
    const cut = await prisma.cut.findUnique({ where: { id: cutId } });
    if (!cut) throw new AppError(404, 'Cut not found', 'CUT_NOT_FOUND');

    const polygons = parseGeoJsonPolygon(cut.geojson);

    const allLocations = await prisma.location.findMany({
      // latitude/longitude are non-nullable in schema
      select: {
        id: true,
        latitude: true,
        longitude: true,
        addresses: { select: { id: true } },
      },
    });

    // Filter locations within polygons and collect all address IDs
    const addressIds: string[] = [];
    for (const loc of allLocations) {
      if (polygons.some((p) => isPointInPolygon(Number(loc.latitude), Number(loc.longitude), p))) {
        addressIds.push(...loc.addresses.map((a) => a.id));
      }
    }

    const [totalAddresses, visitedAddresses, totalVisits, byOutcome] = await Promise.all([
      Promise.resolve(addressIds.length),
      prisma.canvassVisit.findMany({
        where: { addressId: { in: addressIds } },
        distinct: ['addressId'],
        select: { addressId: true },
      }),
      prisma.canvassVisit.count({ where: { addressId: { in: addressIds } } }),
      prisma.canvassVisit.groupBy({
        by: ['outcome'],
        where: { addressId: { in: addressIds } },
        _count: true,
      }),
    ]);

    const outcomeMap: Record<string, number> = {};
    for (const row of byOutcome) {
      outcomeMap[row.outcome] = row._count;
    }

    return {
      cutId,
      cutName: cut.name,
      totalLocations: totalAddresses,
      visitedLocations: visitedAddresses.length,
      completionPercentage: cut.completionPercentage,
      totalVisits,
      byOutcome: outcomeMap,
    };
  },

  async getAdminActivity(filters: AdminActivityInput) {
    const { page, limit, cutId, userId, outcome } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.CanvassVisitWhereInput = {};
    if (userId) where.userId = userId;
    if (outcome) where.outcome = outcome;

    // If filtering by cut, we need to find addresses in that cut
    if (cutId) {
      const cut = await prisma.cut.findUnique({ where: { id: cutId } });
      if (cut) {
        const polygons = parseGeoJsonPolygon(cut.geojson);
        const allLocations = await prisma.location.findMany({
          // latitude/longitude are non-nullable in schema
          select: {
            id: true,
            latitude: true,
            longitude: true,
            addresses: { select: { id: true } },
          },
        });
        const addressIds: string[] = [];
        for (const loc of allLocations) {
          if (polygons.some((p) => isPointInPolygon(Number(loc.latitude), Number(loc.longitude), p))) {
            addressIds.push(...loc.addresses.map((a) => a.id));
          }
        }
        where.addressId = { in: addressIds };
      }
    }

    const [visits, total] = await Promise.all([
      prisma.canvassVisit.findMany({
        where,
        skip,
        take: limit,
        orderBy: { visitedAt: 'desc' },
        include: {
          address: {
            select: {
              id: true,
              unitNumber: true,
              location: { select: { address: true } },
            },
          },
          user: { select: { id: true, name: true, email: true } },
        },
      }),
      prisma.canvassVisit.count({ where }),
    ]);

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

  async getVolunteers() {
    const volunteers = await prisma.canvassVisit.groupBy({
      by: ['userId'],
      _count: true,
      _max: { visitedAt: true },
    });

    const userIds = volunteers.map((v) => v.userId);
    const users = await prisma.user.findMany({
      where: { id: { in: userIds } },
      select: { id: true, name: true, email: true },
    });

    const userMap = new Map(users.map((u) => [u.id, u]));

    const sessions = await prisma.canvassSession.groupBy({
      by: ['userId'],
      where: { userId: { in: userIds } },
      _count: true,
    });
    const sessionMap = new Map(sessions.map((s) => [s.userId, s._count]));

    return volunteers.map((v) => ({
      userId: v.userId,
      name: userMap.get(v.userId)?.name ?? null,
      email: userMap.get(v.userId)?.email ?? '',
      totalVisits: v._count,
      sessions: sessionMap.get(v.userId) ?? 0,
      lastActive: v._max.visitedAt,
    }));
  },

  async getVolunteerStats(userId: string) {
    return this.getMyStats(userId);
  },

  async getAdminVisits(filters: AdminVisitsInput) {
    const { page, limit, cutId, userId, shiftId, outcome, sortBy, sortOrder } = filters;
    const skip = (page - 1) * limit;

    const where: Prisma.CanvassVisitWhereInput = {};
    if (userId) where.userId = userId;
    if (shiftId) where.shiftId = shiftId;
    if (outcome) where.outcome = outcome;

    if (cutId) {
      const cut = await prisma.cut.findUnique({ where: { id: cutId } });
      if (cut) {
        const polygons = parseGeoJsonPolygon(cut.geojson);
        const allLocations = await prisma.location.findMany({
          // latitude/longitude are non-nullable in schema
          select: {
            id: true,
            latitude: true,
            longitude: true,
            addresses: { select: { id: true } },
          },
        });
        const addressIds: string[] = [];
        for (const loc of allLocations) {
          if (polygons.some((p) => isPointInPolygon(Number(loc.latitude), Number(loc.longitude), p))) {
            addressIds.push(...loc.addresses.map((a) => a.id));
          }
        }
        where.addressId = { in: addressIds };
      }
    }

    const [visits, total] = await Promise.all([
      prisma.canvassVisit.findMany({
        where,
        skip,
        take: limit,
        orderBy: { [sortBy]: sortOrder },
        include: {
          address: {
            select: {
              id: true,
              unitNumber: true,
              location: { select: { address: true } },
            },
          },
          user: { select: { id: true, name: true, email: true } },
        },
      }),
      prisma.canvassVisit.count({ where }),
    ]);

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

  async getOutcomeTrends(filters: OutcomeTrendsQueryInput) {
    const { granularity } = filters;
    const dateTo = filters.dateTo ? new Date(filters.dateTo) : new Date();
    const dateFrom = filters.dateFrom
      ? new Date(filters.dateFrom)
      : new Date(dateTo.getTime() - 30 * 24 * 60 * 60 * 1000);

    // Ensure dateTo covers end of day
    const dateToEnd = new Date(dateTo);
    dateToEnd.setHours(23, 59, 59, 999);

    const rows = await prisma.$queryRaw<
      { period: Date; outcome: string; count: number }[]
    >`
      SELECT DATE_TRUNC(${granularity}, "visitedAt") as period,
             outcome::text as outcome,
             COUNT(*)::int as count
      FROM canvass_visits
      WHERE "visitedAt" >= ${dateFrom} AND "visitedAt" <= ${dateToEnd}
      GROUP BY period, outcome
      ORDER BY period ASC
    `;

    // Pivot rows into series: [{ date, NOT_HOME: n, SPOKE_WITH: n, ... }]
    const pivotMap = new Map<string, Record<string, number>>();
    const totals: Record<string, number> = {};

    for (const row of rows) {
      const dateStr = row.period.toISOString().split('T')[0];
      if (!pivotMap.has(dateStr)) {
        pivotMap.set(dateStr, {});
      }
      pivotMap.get(dateStr)![row.outcome] = row.count;
      totals[row.outcome] = (totals[row.outcome] || 0) + row.count;
    }

    const series = Array.from(pivotMap.entries()).map(([date, outcomes]) => ({
      date,
      ...outcomes,
    }));

    return {
      granularity,
      dateFrom: dateFrom.toISOString().split('T')[0],
      dateTo: dateTo.toISOString().split('T')[0],
      series,
      totals,
    };
  },

  // ─── Helpers ───────────────────────────────────────────────────────

  async recalculateCutCompletion(cutId: string) {
    const cut = await prisma.cut.findUnique({ where: { id: cutId } });
    if (!cut) return;

    const polygons = parseGeoJsonPolygon(cut.geojson);

    const allLocations = await prisma.location.findMany({
      // latitude/longitude are non-nullable in schema
      select: {
        id: true,
        latitude: true,
        longitude: true,
        addresses: { select: { id: true } },
      },
    });

    // Filter locations within polygons and collect all address IDs
    const addressIds: string[] = [];
    for (const loc of allLocations) {
      if (polygons.some((p) => isPointInPolygon(Number(loc.latitude), Number(loc.longitude), p))) {
        addressIds.push(...loc.addresses.map((a) => a.id));
      }
    }

    if (addressIds.length === 0) return;

    const visitedAddresses = await prisma.canvassVisit.findMany({
      where: { addressId: { in: addressIds } },
      distinct: ['addressId'],
      select: { addressId: true },
    });

    const pct = Math.round((visitedAddresses.length / addressIds.length) * 100);

    await prisma.cut.update({
      where: { id: cutId },
      data: {
        completionPercentage: pct,
        lastCanvassed: new Date(),
      },
    });
  },

  async closeAbandonedSessions() {
    const cutoff = new Date(Date.now() - 12 * 60 * 60 * 1000); // 12 hours ago
    const result = await prisma.canvassSession.updateMany({
      where: {
        status: CanvassSessionStatus.ACTIVE,
        startedAt: { lt: cutoff },
      },
      data: {
        status: CanvassSessionStatus.ABANDONED,
        endedAt: new Date(),
      },
    });

    if (result.count > 0) {
      logger.info(`Closed ${result.count} abandoned canvass sessions`);
    }

    return result.count;
  },
};
