import type { Logger } from 'pino';
import { connection as redis } from './queueService.server';
import { googleGeocodingService, GoogleGeocodingService } from './googleGeocodingService.server';
import { nominatimGeocodingService, NominatimGeocodingService } from './nominatimGeocodingService.server';

export class GeocodingService {
  constructor(
    private googleService: GoogleGeocodingService,
    private nominatimService: NominatimGeocodingService
  ) {}

  async geocodeAddress(address: string, logger: Logger): Promise<{ lat: number; lng: number } | null> {
    const cacheKey = `geocode:${address}`;

    try {
      const cached = await redis.get(cacheKey);
      if (cached) {
        logger.info({ cacheKey }, 'Geocoding result found in cache.');
        return JSON.parse(cached);
      }
    } catch (error) {
      logger.error({ err: error, cacheKey }, 'Redis GET or JSON.parse command failed. Proceeding without cache.');
    }

    if (process.env.GOOGLE_MAPS_API_KEY) {
      try {
        const coordinates = await this.googleService.geocode(address, logger);
        if (coordinates) {
          await this.setCache(cacheKey, coordinates, logger);
          return coordinates;
        }
        logger.info({ address, provider: 'Google' }, 'Google Geocoding returned no results. Falling back to Nominatim.');
      } catch (error) {
        logger.error({ err: error }, 'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.');
      }
    } else {
      logger.warn('GOOGLE_MAPS_API_KEY is not set. Falling back to Nominatim as the primary geocoding provider.');
    }

    const nominatimResult = await this.nominatimService.geocode(address, logger);
    if (nominatimResult) {
      await this.setCache(cacheKey, nominatimResult, logger);
      return nominatimResult;
    }

    logger.error({ address }, 'All geocoding providers failed.');
    return null;
  }

  private async setCache(cacheKey: string, result: { lat: number; lng: number }, logger: Logger): Promise<void> {
    try {
      await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
    } catch (error) {
      logger.error({ err: error, cacheKey }, 'Redis SET command failed. Result will not be cached.');
    }
  }

  async clearGeocodeCache(logger: Logger): Promise<number> {
    let cursor = '0';
    let totalDeleted = 0;
    const pattern = 'geocode:*';
    logger.info(`Starting geocode cache clear with pattern: ${pattern}`);

    try {
      do {
        const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
        cursor = nextCursor;
        if (keys.length > 0) {
          const deletedCount = await redis.del(keys);
          totalDeleted += deletedCount;
        }
      } while (cursor !== '0');

      logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
      return totalDeleted;
    } catch (error) {
      logger.error({ err: error }, 'Failed to clear geocode cache from Redis.');
      throw error;
    }
  }
}

export const geocodingService = new GeocodingService(googleGeocodingService, nominatimGeocodingService);