import { Injectable, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

interface NominatimPlace {
  lat: string;
  lon: string;
  display_name: string;
  name?: string;
}

interface NominatimReverse {
  display_name?: string;
  address?: {
    road?: string;
    neighbourhood?: string;
    suburb?: string;
    city?: string;
    town?: string;
    village?: string;
    county?: string;
    state?: string;
    country?: string;
    building?: string;
    amenity?: string;
  };
}

@Injectable()
export class GeocodingService {
  private lastRequestTime = 0;
  private reverseCache = new Map<string, string>();
  private readonly userAgent: string;

  constructor(private readonly configService: ConfigService) {
    this.userAgent = this.configService.get('brand.geocodingUserAgent', 'FFAIWorkspace-SiteAttendance/1.0');
  }

  private async rateLimitedFetch(url: string): Promise<Response> {
    const now = Date.now();
    const elapsed = now - this.lastRequestTime;
    if (elapsed < 1500) {
      await new Promise((r) => setTimeout(r, 1500 - elapsed));
    }
    this.lastRequestTime = Date.now();
    return fetch(url, {
      headers: {
        Accept: 'application/json',
        'User-Agent': this.userAgent,
      },
    });
  }

  async searchPlaces(query: string, lang: string = 'zh-CN,zh,en') {
    if (!query || query.trim().length < 2) {
      throw new BadRequestException('Query must be at least 2 characters');
    }

    const url = new URL('https://nominatim.openstreetmap.org/search');
    url.searchParams.set('q', query.trim());
    url.searchParams.set('format', 'jsonv2');
    url.searchParams.set('addressdetails', '1');
    url.searchParams.set('limit', '5');
    url.searchParams.set('accept-language', lang);

    const response = await this.rateLimitedFetch(url.toString());
    if (!response.ok) {
      return { places: [] };
    }

    const data = (await response.json()) as NominatimPlace[];
    return {
      places: data.map((item) => ({
        latitude: Number(item.lat),
        longitude: Number(item.lon),
        displayName: item.display_name,
        title: item.name || item.display_name.split(',')[0] || item.display_name,
      })),
    };
  }

  async reverseGeocode(lat: number, lon: number, lang: string = 'zh-CN,zh,en') {
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
      return { displayName: '' };
    }

    // Check memory cache
    const cacheKey = `${lat.toFixed(5)},${lon.toFixed(5)},${lang}`;
    const cached = this.reverseCache.get(cacheKey);
    if (cached) return { displayName: cached };

    const url = new URL('https://nominatim.openstreetmap.org/reverse');
    url.searchParams.set('format', 'jsonv2');
    url.searchParams.set('lat', String(lat));
    url.searchParams.set('lon', String(lon));
    url.searchParams.set('zoom', '18');
    url.searchParams.set('addressdetails', '1');
    url.searchParams.set('accept-language', lang);

    // Retry on 429
    let response: Response | null = null;
    for (let attempt = 0; attempt < 3; attempt++) {
      response = await this.rateLimitedFetch(url.toString());
      if (response.status !== 429) break;
      await new Promise((r) => setTimeout(r, 2000 * (attempt + 1)));
    }

    if (!response || !response.ok) {
      return { displayName: `${lat.toFixed(5)}, ${lon.toFixed(5)}` };
    }

    const data = (await response.json()) as NominatimReverse;

    const addr = data.address;
    let concise = '';
    if (addr) {
      const parts = [
        addr.building || addr.amenity,
        addr.road,
        addr.neighbourhood || addr.suburb,
        addr.city || addr.town || addr.village,
        addr.state,
      ].filter(Boolean);
      concise = parts.join(', ');
    }

    const displayName = concise || data.display_name || `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
    this.reverseCache.set(cacheKey, displayName);
    return { displayName };
  }
}
