import { Queue, Worker, Job } from 'bullmq';
import { LocationHistoryAction, Prisma } from '@prisma/client';
import { redis } from '../config/redis';
import { prisma } from '../config/database';
import { geocodingService } from '../modules/map/geocoding/geocoding.service';
import { logger } from '../utils/logger';
import { env } from '../config/env';

interface BulkGeocodeJobData {
  userId: string;
  filters: {
    confidenceThreshold?: number;
    buildingType?: string;
    cutId?: string;
    limit?: number;
  };
  timestamp: number;
}

interface BulkGeocodeProgress {
  total: number;
  processed: number;
  improved: number;
  failed: number;
  unchanged: number;
  currentAddress?: string;
}

interface BulkGeocodeResult {
  total: number;
  processed: number;
  improved: number;
  failed: number;
  unchanged: number;
  duration: number;
}

class GeocodeQueueService {
  private queue: Queue<BulkGeocodeJobData, BulkGeocodeResult, string>;
  private worker: Worker<BulkGeocodeJobData, BulkGeocodeResult, string> | null = null;

  constructor() {
    this.queue = new Queue<BulkGeocodeJobData, BulkGeocodeResult, string>('bulk-geocode', {
      connection: {
        host: redis.options.host,
        port: redis.options.port || 6379,
        password: redis.options.password,
      },
    });
  }

  async startWorker() {
    if (this.worker || env.BULK_GEOCODE_ENABLED !== 'true') {
      return;
    }

    this.worker = new Worker<BulkGeocodeJobData, BulkGeocodeResult, string>(
      'bulk-geocode',
      async (job: Job<BulkGeocodeJobData, BulkGeocodeResult, string>) => {
        return this.processBulkGeocode(job);
      },
      {
        connection: {
          host: redis.options.host,
          port: redis.options.port || 6379,
          password: redis.options.password,
        },
        concurrency: 1, // Process one bulk job at a time
      }
    );

    this.worker.on('completed', (job) => {
      logger.info(`Bulk geocode job ${job.id} completed: ${JSON.stringify(job.returnvalue)}`);
    });

    this.worker.on('failed', (job, err) => {
      logger.error(`Bulk geocode job ${job?.id} failed:`, err);
    });

    logger.info('Bulk geocode worker started');
  }

  async close() {
    if (this.worker) {
      await this.worker.close();
    }
    await this.queue.close();
  }

  async startBulkGeocode(userId: string, filters: BulkGeocodeJobData['filters']): Promise<string> {
    // Validate limit
    const limit = Math.min(filters.limit || 1000, env.BULK_GEOCODE_MAX_BATCH);

    const job = await this.queue.add('bulk-geocode', {
      userId,
      filters: { ...filters, limit },
      timestamp: Date.now(),
    });

    return job.id as string;
  }

  async getJobStatus(jobId: string) {
    const job = await this.queue.getJob(jobId);
    if (!job) {
      throw new Error('Job not found');
    }

    const state = await job.getState();
    const progress = job.progress as BulkGeocodeProgress | undefined;

    return {
      id: job.id,
      state,
      progress,
      result: job.returnvalue,
      failedReason: job.failedReason,
    };
  }

  async getStats() {
    const [waiting, active, completed, failed] = await Promise.all([
      this.queue.getWaitingCount(),
      this.queue.getActiveCount(),
      this.queue.getCompletedCount(),
      this.queue.getFailedCount(),
    ]);

    return { waiting, active, completed, failed };
  }

  private async processBulkGeocode(job: Job<BulkGeocodeJobData, BulkGeocodeResult, string>): Promise<BulkGeocodeResult> {
    const { userId, filters } = job.data;
    const startTime = Date.now();

    logger.info(`Starting bulk geocode job ${job.id} with filters: ${JSON.stringify(filters)}`);

    // Build query filters
    const where: Prisma.LocationWhereInput = {};

    // Confidence threshold filter
    if (filters.confidenceThreshold !== undefined) {
      where.OR = [
        { geocodeConfidence: { lt: filters.confidenceThreshold } },
        { geocodeConfidence: null },
      ];
    }

    // Building type filter
    if (filters.buildingType) {
      where.buildingType = filters.buildingType as any;
    }

    // Cut boundary filter (point-in-polygon)
    if (filters.cutId) {
      const cut = await prisma.cut.findUnique({
        where: { id: filters.cutId },
        select: { geojson: true },
      });

      if (cut?.geojson) {
        // Note: This is a simplified version. For production, you'd want to use PostGIS
        // or pre-filter by bounding box before applying point-in-polygon
        const geojson = cut.geojson as any;
        if (geojson.type === 'Polygon' && geojson.coordinates) {
          // We'll apply point-in-polygon filtering after fetching
          logger.info(`Will apply point-in-polygon filtering for cut ${filters.cutId}`);
        }
      }
    }

    // Fetch locations to geocode
    const locations = await prisma.location.findMany({
      where,
      select: {
        id: true,
        address: true,
        latitude: true,
        longitude: true,
        geocodeConfidence: true,
      },
      take: filters.limit || 1000,
      orderBy: [
        { geocodeConfidence: 'asc' }, // Lowest confidence first
        { updatedAt: 'asc' },
      ],
    });

    // Apply cut filtering if needed (point-in-polygon)
    let filteredLocations = locations;
    if (filters.cutId) {
      const cut = await prisma.cut.findUnique({
        where: { id: filters.cutId },
        select: { geojson: true },
      });

      if (cut?.geojson) {
        const { isPointInPolygon, parseGeoJsonPolygon } = await import('../utils/spatial');
        const polygons = parseGeoJsonPolygon(cut.geojson as any);
        if (polygons && polygons.length > 0) {
          filteredLocations = locations.filter((loc) => {
            if (!loc.latitude || !loc.longitude) return false;
            const lat = Number(loc.latitude);
            const lng = Number(loc.longitude);
            return polygons.some((polygon) => isPointInPolygon(lat, lng, polygon));
          });
        }
      }
    }

    const total = filteredLocations.length;
    let processed = 0;
    let improved = 0;
    let failed = 0;
    let unchanged = 0;

    logger.info(`Processing ${total} locations for bulk geocode`);

    // Process each location
    for (const location of filteredLocations) {
      if (!location.address) {
        unchanged++;
        processed++;
        continue;
      }

      try {
        // Update progress
        await job.updateProgress({
          total,
          processed,
          improved,
          failed,
          unchanged,
          currentAddress: location.address,
        } as BulkGeocodeProgress);

        // Geocode the address
        const result = await geocodingService.geocode(location.address);

        if (result) {
          const oldConfidence = location.geocodeConfidence || 0;
          const newConfidence = result.confidence;

          // Only update if confidence improved
          if (newConfidence > oldConfidence) {
            await prisma.$transaction(async (tx) => {
              // Update location
              await tx.location.update({
                where: { id: location.id },
                data: {
                  latitude: result.latitude,
                  longitude: result.longitude,
                  geocodeConfidence: result.confidence,
                  geocodeProvider: result.provider,
                },
              });

              // Record history
              await tx.locationHistory.create({
                data: {
                  locationId: location.id,
                  userId,
                  action: LocationHistoryAction.BULK_GEOCODED,
                  field: null,
                  oldValue: null,
                  newValue: null,
                  metadata: {
                    provider: result.provider,
                    oldConfidence,
                    newConfidence: result.confidence,
                    improved: true,
                  } as unknown as Prisma.InputJsonValue,
                },
              });
            });

            improved++;
          } else {
            unchanged++;
          }
        } else {
          failed++;
        }
      } catch (error) {
        logger.error(`Failed to geocode location ${location.id}:`, error);
        failed++;
      }

      processed++;
    }

    const duration = Math.round((Date.now() - startTime) / 1000);

    logger.info(
      `Bulk geocode job ${job.id} completed: ${improved} improved, ${unchanged} unchanged, ${failed} failed in ${duration}s`
    );

    return {
      total,
      processed,
      improved,
      failed,
      unchanged,
      duration,
    };
  }
}

export const geocodeQueueService = new GeocodeQueueService();
