import { Prisma } from '@prisma/client';
import { prisma } from '../../../config/database';
import { haversineDistance } from '../../../utils/spatial';
import { logger } from '../../../utils/logger';
import type {
  StartTrackingInput,
  SubmitPointsInput,
  LinkCanvassInput,
  LiveVolunteersQueryInput,
  HistoricalListQueryInput,
} from './tracking.schemas';

const MAX_DISTANCE_JUMP_M = 1000; // Skip distance calc for GPS glitches > 1km

class TrackingService {
  /** Start or resume a tracking session for a volunteer. */
  async startSession(userId: string, data: StartTrackingInput) {
    // Check for existing active session — reuse it
    const existing = await prisma.trackingSession.findFirst({
      where: { userId, isActive: true },
    });
    if (existing) return existing;

    return prisma.trackingSession.create({
      data: {
        userId,
        canvassSessionId: data.canvassSessionId ?? null,
        lastLatitude: data.latitude != null ? new Prisma.Decimal(data.latitude) : null,
        lastLongitude: data.longitude != null ? new Prisma.Decimal(data.longitude) : null,
        lastRecordedAt: data.latitude != null ? new Date() : null,
      },
    });
  }

  /** End a tracking session. */
  async endSession(sessionId: string, userId: string) {
    const session = await prisma.trackingSession.findFirst({
      where: { id: sessionId, userId, isActive: true },
    });
    if (!session) return null;

    return prisma.trackingSession.update({
      where: { id: sessionId },
      data: { isActive: false, endedAt: new Date() },
    });
  }

  /** Submit a batch of GPS points for a tracking session. */
  async submitPoints(sessionId: string, userId: string, data: SubmitPointsInput) {
    const session = await prisma.trackingSession.findFirst({
      where: { id: sessionId, userId, isActive: true },
    });
    if (!session) {
      throw Object.assign(new Error('Active tracking session not found'), { statusCode: 404 });
    }

    const { points } = data;

    // Batch insert all points
    await prisma.trackPoint.createMany({
      data: points.map((p) => ({
        trackingSessionId: sessionId,
        latitude: new Prisma.Decimal(p.latitude),
        longitude: new Prisma.Decimal(p.longitude),
        accuracy: p.accuracy ?? null,
        recordedAt: new Date(p.recordedAt),
        eventType: p.eventType ?? null,
      })),
    });

    // Calculate incremental distance
    let addedDistance = 0;
    let prevLat = session.lastLatitude ? Number(session.lastLatitude) : null;
    let prevLng = session.lastLongitude ? Number(session.lastLongitude) : null;

    // Sort points by recordedAt for correct distance calculation
    const sorted = [...points].sort(
      (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime(),
    );

    for (const p of sorted) {
      if (prevLat != null && prevLng != null) {
        const d = haversineDistance(prevLat, prevLng, p.latitude, p.longitude);
        if (d <= MAX_DISTANCE_JUMP_M) {
          addedDistance += d;
        }
      }
      prevLat = p.latitude;
      prevLng = p.longitude;
    }

    const lastPoint = sorted[sorted.length - 1]!;

    // Update session summary fields
    await prisma.trackingSession.update({
      where: { id: sessionId },
      data: {
        totalPoints: { increment: points.length },
        totalDistanceM: { increment: addedDistance },
        lastLatitude: new Prisma.Decimal(lastPoint.latitude),
        lastLongitude: new Prisma.Decimal(lastPoint.longitude),
        lastRecordedAt: new Date(lastPoint.recordedAt),
      },
    });

    return { inserted: points.length, addedDistanceM: Math.round(addedDistance) };
  }

  /** Get the active tracking session for a user. */
  async getActiveSession(userId: string) {
    return prisma.trackingSession.findFirst({
      where: { userId, isActive: true },
    });
  }

  /** Link a canvass session to an existing tracking session (ownership-verified). */
  async linkCanvassSession(trackingSessionId: string, userId: string, data: LinkCanvassInput) {
    const session = await prisma.trackingSession.findFirst({
      where: { id: trackingSessionId, userId, isActive: true },
    });
    if (!session) {
      throw Object.assign(new Error('Active tracking session not found'), { statusCode: 404 });
    }
    return prisma.trackingSession.update({
      where: { id: trackingSessionId },
      data: { canvassSessionId: data.canvassSessionId },
    });
  }

  /** Get all active volunteers with their recent trail for the admin live map. */
  async getLiveVolunteers(query: LiveVolunteersQueryInput) {
    const cutoff = new Date(Date.now() - query.staleCutoffMinutes * 60 * 1000);

    const sessions = await prisma.trackingSession.findMany({
      where: {
        isActive: true,
        lastRecordedAt: { gte: cutoff },
      },
      include: {
        user: { select: { id: true, name: true, email: true } },
      },
      orderBy: { lastRecordedAt: 'desc' },
    });

    // For each session, fetch last 20 track points for the trail polyline
    const results = await Promise.all(
      sessions.map(async (s) => {
        const recentPoints = await prisma.trackPoint.findMany({
          where: { trackingSessionId: s.id },
          orderBy: { recordedAt: 'desc' },
          take: 20,
          select: { latitude: true, longitude: true },
        });

        return {
          userId: s.user.id,
          name: s.user.name,
          email: s.user.email,
          latitude: Number(s.lastLatitude),
          longitude: Number(s.lastLongitude),
          lastRecordedAt: s.lastRecordedAt?.toISOString() ?? s.startedAt.toISOString(),
          recentTrail: recentPoints.reverse().map((p) => [Number(p.latitude), Number(p.longitude)] as [number, number]),
          canvassSessionId: s.canvassSessionId,
          trackingSessionId: s.id,
        };
      }),
    );

    return results;
  }

  /** Get the full route for a tracking session, verifying ownership. */
  async getMySessionRoute(trackingSessionId: string, userId: string) {
    const session = await prisma.trackingSession.findFirst({
      where: { id: trackingSessionId, userId },
    });
    if (!session) return null;
    return this.getSessionRoute(trackingSessionId);
  }

  /** Get the full route for a tracking session. */
  async getSessionRoute(trackingSessionId: string) {
    const points = await prisma.trackPoint.findMany({
      where: { trackingSessionId },
      orderBy: { recordedAt: 'asc' },
      select: {
        latitude: true,
        longitude: true,
        accuracy: true,
        recordedAt: true,
        eventType: true,
      },
    });

    return {
      coordinates: points.map((p) => [Number(p.latitude), Number(p.longitude)] as [number, number]),
      events: points
        .filter((p) => p.eventType != null)
        .map((p) => ({
          latitude: Number(p.latitude),
          longitude: Number(p.longitude),
          eventType: p.eventType!,
          recordedAt: p.recordedAt.toISOString(),
        })),
      totalPoints: points.length,
    };
  }

  /** Paginated list of historical tracking sessions. */
  async getHistoricalSessions(query: HistoricalListQueryInput) {
    const where: Prisma.TrackingSessionWhereInput = {};

    if (query.userId) where.userId = query.userId;
    if (query.from || query.to) {
      where.startedAt = {};
      if (query.from) where.startedAt.gte = new Date(query.from);
      if (query.to) where.startedAt.lte = new Date(query.to);
    }

    const [sessions, total] = await Promise.all([
      prisma.trackingSession.findMany({
        where,
        include: {
          user: { select: { id: true, name: true, email: true } },
        },
        orderBy: { startedAt: 'desc' },
        skip: (query.page - 1) * query.limit,
        take: query.limit,
      }),
      prisma.trackingSession.count({ where }),
    ]);

    return {
      sessions: sessions.map((s) => ({
        id: s.id,
        userId: s.user.id,
        userName: s.user.name,
        userEmail: s.user.email,
        startedAt: s.startedAt.toISOString(),
        endedAt: s.endedAt?.toISOString() ?? null,
        totalPoints: s.totalPoints,
        totalDistanceM: s.totalDistanceM,
        canvassSessionId: s.canvassSessionId,
        isActive: s.isActive,
      })),
      pagination: {
        page: query.page,
        limit: query.limit,
        total,
        totalPages: Math.ceil(total / query.limit),
      },
    };
  }

  /** Delete old track points and empty sessions. */
  async cleanupOldData(retentionDays: number) {
    const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);

    const { count: deletedPoints } = await prisma.trackPoint.deleteMany({
      where: { recordedAt: { lt: cutoff } },
    });

    // Delete sessions with zero remaining points that are inactive
    const emptySessions = await prisma.trackingSession.findMany({
      where: {
        isActive: false,
        trackPoints: { none: {} },
      },
      select: { id: true },
    });

    if (emptySessions.length > 0) {
      await prisma.trackingSession.deleteMany({
        where: { id: { in: emptySessions.map((s) => s.id) } },
      });
    }

    if (deletedPoints > 0 || emptySessions.length > 0) {
      logger.info(`Tracking cleanup: deleted ${deletedPoints} points, ${emptySessions.length} empty sessions`);
    }
  }

  /** Mark stale tracking sessions as inactive. */
  async closeStaleTrackingSessions(minutes: number) {
    const cutoff = new Date(Date.now() - minutes * 60 * 1000);

    const { count } = await prisma.trackingSession.updateMany({
      where: {
        isActive: true,
        OR: [
          { lastRecordedAt: { lt: cutoff } },
          { lastRecordedAt: null, startedAt: { lt: cutoff } },
        ],
      },
      data: { isActive: false, endedAt: new Date() },
    });

    if (count > 0) {
      logger.info(`Closed ${count} stale tracking sessions (no data for ${minutes}min)`);
    }
  }
}

export const trackingService = new TrackingService();
