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

interface LatLng {
  latitude: number;
  longitude: number;
}

export interface GeocodeResult {
  formattedAddress: string;
  latitude: number;
  longitude: number;
  placeId?: string | null;
  postalCode?: string | null;
  city?: string | null;
  state?: string | null;
  neighborhood?: string | null;
}

type AddressInput = {
  street?: string | null;
  number?: string | null;
  complement?: string | null;
  neighborhood?: string | null;
  city?: string | null;
  state?: string | null;
  zipCode?: string | null;
  label?: string | null;
};

export interface RouteResult {
  distanceKm: number;
  durationSeconds: number;
  durationMinutes: number;
  encodedPolyline: string;
  provider: 'google-routes' | 'google-directions' | 'fallback';
  legs?: unknown[];
}

@Injectable()
export class MapsService {
  private readonly apiKey?: string;
  private readonly language: string;
  private readonly region: string;
  private readonly useRoutesApi: boolean;

  constructor(private readonly configService: ConfigService) {
    this.apiKey = this.configService.get<string>('GOOGLE_MAPS_API_KEY') || undefined;
    this.language = this.configService.get<string>('GOOGLE_MAPS_LANGUAGE') ?? 'pt-BR';
    this.region = this.configService.get<string>('GOOGLE_MAPS_REGION') ?? 'BR';
    this.useRoutesApi = this.configService.get<boolean>('GOOGLE_MAPS_USE_ROUTES_API') ?? true;
  }

  isConfigured() {
    return Boolean(this.apiKey);
  }

  async geocodeAddress(address: string): Promise<GeocodeResult | null> {
    if (!this.apiKey) {
      return this.geocodeAddressWithNominatim(address);
    }

    const url = new URL('https://maps.googleapis.com/maps/api/geocode/json');
    url.searchParams.set('address', address);
    url.searchParams.set('language', this.language);
    url.searchParams.set('region', this.region);
    url.searchParams.set('key', this.apiKey);

    const response = await fetch(url, { method: 'GET' });
    if (!response.ok) {
      throw new InternalServerErrorException('Falha ao consultar o Geocoding API.');
    }

    const payload = await response.json();
    const result = payload.results?.[0];
    if (!result?.geometry?.location) {
      return null;
    }

    const components = Array.isArray(result.address_components) ? result.address_components : [];
    const findComponent = (...types: string[]) =>
      components.find((component: { types?: string[] }) => types.every((type) => component.types?.includes(type)));

    return {
      formattedAddress: result.formatted_address,
      latitude: Number(result.geometry.location.lat),
      longitude: Number(result.geometry.location.lng),
      placeId: result.place_id ?? null,
      postalCode: findComponent('postal_code')?.long_name ?? null,
      city:
        findComponent('administrative_area_level_2', 'political')?.long_name ??
        findComponent('locality', 'political')?.long_name ??
        null,
      state: findComponent('administrative_area_level_1', 'political')?.short_name ?? null,
      neighborhood:
        findComponent('sublocality', 'sublocality_level_1', 'political')?.long_name ??
        findComponent('neighborhood', 'political')?.long_name ??
        null,
    };
  }

  private async geocodeAddressWithNominatim(address: string): Promise<GeocodeResult | null> {
    const url = new URL('https://nominatim.openstreetmap.org/search');
    url.searchParams.set('q', address);
    url.searchParams.set('format', 'jsonv2');
    url.searchParams.set('addressdetails', '1');
    url.searchParams.set('limit', '1');
    url.searchParams.set('countrycodes', 'br');

    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'User-Agent': 'CHEGOOU/1.0 (delivery geocoding)',
        Accept: 'application/json',
      },
    });

    if (!response.ok) {
      throw new InternalServerErrorException('Falha ao consultar o geocoding de fallback.');
    }

    const payload = (await response.json()) as Array<{
      lat?: string;
      lon?: string;
      display_name?: string;
      place_id?: string | number;
      address?: Record<string, string | undefined>;
    }>;

    const result = payload?.[0];
    if (!result?.lat || !result?.lon) {
      return null;
    }

    const addressData = result.address ?? {};

    return {
      formattedAddress: result.display_name ?? address,
      latitude: Number(result.lat),
      longitude: Number(result.lon),
      placeId: result.place_id ? String(result.place_id) : null,
      postalCode: addressData.postcode ?? null,
      city: addressData.city ?? addressData.town ?? addressData.municipality ?? addressData.county ?? null,
      state: addressData.state_code ?? addressData.state ?? null,
      neighborhood: addressData.suburb ?? addressData.neighbourhood ?? addressData.city_district ?? null,
    };
  }

  async geocodeAddressStrict(input: AddressInput): Promise<GeocodeResult | null> {
    if (!this.apiKey) {
      return null;
    }

    const zipCode = this.normalizeZipCode(input.zipCode);
    const city = this.normalizeToken(input.city);
    const state = this.normalizeToken(input.state);
    const neighborhood = this.normalizeToken(input.neighborhood);

    const candidates = [
      [
        [input.street, input.number].filter(Boolean).join(', '),
        input.complement,
        input.neighborhood,
        input.city,
        input.state,
        input.zipCode,
        input.label,
      ]
        .filter(Boolean)
        .join(', '),
      zipCode ? `${zipCode}, Brasil` : '',
      [input.street, input.number].filter(Boolean).join(', '),
    ].filter(Boolean);

    for (const query of candidates) {
      const result = await this.geocodeAddress(query);
      if (!result) {
        continue;
      }

      const resultZip = this.normalizeZipCode(result.postalCode);
      const resultCity = this.normalizeToken(result.city);
      const resultState = this.normalizeToken(result.state);
      const resultNeighborhood = this.normalizeToken(result.neighborhood);

      const zipMatches = !zipCode || (resultZip && resultZip === zipCode);
      const cityMatches = !city || (resultCity && resultCity.includes(city));
      const stateMatches = !state || (resultState && resultState === state);
      const neighborhoodMatches = !neighborhood || !resultNeighborhood || resultNeighborhood.includes(neighborhood);

      if (zipMatches && cityMatches && stateMatches && neighborhoodMatches) {
        return result;
      }
    }

    return null;
  }

  async getDistanceMatrix(origin: LatLng, destination: LatLng) {
    if (!this.apiKey) {
      const distanceKm = this.haversineDistance(origin.latitude, origin.longitude, destination.latitude, destination.longitude);
      return {
        distanceKm,
        durationSeconds: Math.round((distanceKm / 28) * 3600),
        durationMinutes: Math.max(2, Math.round((distanceKm / 28) * 60)),
        provider: 'fallback' as const,
      };
    }

    const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json');
    url.searchParams.set('origins', `${origin.latitude},${origin.longitude}`);
    url.searchParams.set('destinations', `${destination.latitude},${destination.longitude}`);
    url.searchParams.set('mode', 'driving');
    url.searchParams.set('language', this.language);
    url.searchParams.set('region', this.region);
    url.searchParams.set('departure_time', 'now');
    url.searchParams.set('key', this.apiKey);

    const response = await fetch(url, { method: 'GET' });
    if (!response.ok) {
      throw new InternalServerErrorException('Falha ao consultar o Distance Matrix API.');
    }

    const payload = await response.json();
    const element = payload.rows?.[0]?.elements?.[0];
    if (!element || element.status !== 'OK') {
      return null;
    }

    const durationSeconds = Number(element.duration_in_traffic?.value ?? element.duration?.value ?? 0);
    return {
      distanceKm: Number((Number(element.distance?.value ?? 0) / 1000).toFixed(2)),
      durationSeconds,
      durationMinutes: Math.max(2, Math.round(durationSeconds / 60)),
      provider: 'google-directions' as const,
    };
  }

  async getRoute(origin: LatLng, destination: LatLng): Promise<RouteResult> {
    if (!this.apiKey) {
      const distanceKm = this.haversineDistance(origin.latitude, origin.longitude, destination.latitude, destination.longitude);
      return {
        distanceKm,
        durationSeconds: Math.round((distanceKm / 28) * 3600),
        durationMinutes: Math.max(2, Math.round((distanceKm / 28) * 60)),
        encodedPolyline: JSON.stringify([[origin.latitude, origin.longitude], [destination.latitude, destination.longitude]]),
        provider: 'fallback',
      };
    }

    if (this.useRoutesApi) {
      const route = await this.computeRoutesApi(origin, destination);
      if (route) {
        return route;
      }
    }

    return this.computeDirectionsApi(origin, destination);
  }

  private async computeRoutesApi(origin: LatLng, destination: LatLng): Promise<RouteResult | null> {
    const response = await fetch('https://routes.googleapis.com/directions/v2:computeRoutes', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Goog-Api-Key': this.apiKey!,
        'X-Goog-FieldMask': 'routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline,routes.legs',
      },
      body: JSON.stringify({
        origin: { location: { latLng: { latitude: origin.latitude, longitude: origin.longitude } } },
        destination: { location: { latLng: { latitude: destination.latitude, longitude: destination.longitude } } },
        travelMode: 'DRIVE',
        routingPreference: 'TRAFFIC_AWARE',
        languageCode: this.language,
      }),
    });

    if (!response.ok) {
      return null;
    }

    const payload = await response.json();
    const route = payload.routes?.[0];
    if (!route) {
      return null;
    }

    const durationSeconds = this.parseDurationSeconds(route.duration);
    return {
      distanceKm: Number((Number(route.distanceMeters ?? 0) / 1000).toFixed(2)),
      durationSeconds,
      durationMinutes: Math.max(2, Math.round(durationSeconds / 60)),
      encodedPolyline: route.polyline?.encodedPolyline ?? '',
      provider: 'google-routes',
      legs: route.legs ?? [],
    };
  }

  private async computeDirectionsApi(origin: LatLng, destination: LatLng): Promise<RouteResult> {
    const url = new URL('https://maps.googleapis.com/maps/api/directions/json');
    url.searchParams.set('origin', `${origin.latitude},${origin.longitude}`);
    url.searchParams.set('destination', `${destination.latitude},${destination.longitude}`);
    url.searchParams.set('mode', 'driving');
    url.searchParams.set('departure_time', 'now');
    url.searchParams.set('language', this.language);
    url.searchParams.set('region', this.region);
    url.searchParams.set('key', this.apiKey!);

    const response = await fetch(url, { method: 'GET' });
    if (!response.ok) {
      throw new InternalServerErrorException('Falha ao consultar o Directions API.');
    }

    const payload = await response.json();
    const route = payload.routes?.[0];
    const leg = route?.legs?.[0];
    if (!route || !leg) {
      throw new InternalServerErrorException('Directions API nao retornou rota.');
    }

    const durationSeconds = Number(leg.duration_in_traffic?.value ?? leg.duration?.value ?? 0);
    return {
      distanceKm: Number((Number(leg.distance?.value ?? 0) / 1000).toFixed(2)),
      durationSeconds,
      durationMinutes: Math.max(2, Math.round(durationSeconds / 60)),
      encodedPolyline: route.overview_polyline?.points ?? '',
      provider: 'google-directions',
      legs: route.legs ?? [],
    };
  }

  private parseDurationSeconds(value?: string) {
    if (!value) {
      return 0;
    }
    return Number(value.replace('s', ''));
  }

  private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number) {
    const toRad = (value: number) => (value * Math.PI) / 180;
    const earthRadiusKm = 6371;
    const dLat = toRad(lat2 - lat1);
    const dLon = toRad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    return Number((earthRadiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))).toFixed(2));
  }

  private normalizeZipCode(value?: string | null) {
    return value ? value.replace(/\D/g, '') : '';
  }

  private normalizeToken(value?: string | null) {
    return value
      ? value
          .normalize('NFD')
          .replace(/[\u0300-\u036f]/g, '')
          .trim()
          .toLowerCase()
      : '';
  }
}
