/// <reference types="google.maps" />

const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY as string | undefined;
const placesKey =
    (import.meta.env.VITE_GOOGLE_PLACES_API_KEY as string | undefined) ||
    apiKey;

let script: Promise<void> | null = null;

export const hasGoogleMaps = Boolean(apiKey);
export const hasPlaces = Boolean(placesKey);

function loadScript(): Promise<void> {
    if (!apiKey) {
        return Promise.reject(new Error('Google Maps API key is missing'));
    }

    script ??= new Promise<void>((resolve, reject) => {
        const callback = '__zoztripMapsReady';
        const element = document.createElement('script');

        (window as unknown as Record<string, () => void>)[callback] = () =>
            resolve();

        element.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&v=weekly&loading=async&auth_referrer_policy=origin&callback=${callback}`;
        element.async = true;
        element.onerror = () => {
            script = null;
            reject(new Error('Google Maps failed to load'));
        };
        document.head.appendChild(element);
    });

    return script;
}

type LatLng = { lat: number; lng: number };

async function placesRequest<T>(
    path: string,
    fieldMask: string,
    body?: unknown,
): Promise<T> {
    if (!placesKey) {
        throw new Error('Google Places API key is missing');
    }

    const response = await fetch(`https://places.googleapis.com/v1/${path}`, {
        method: body === undefined ? 'GET' : 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Goog-Api-Key': placesKey,
            'X-Goog-FieldMask': fieldMask,
        },
        body: body === undefined ? undefined : JSON.stringify(body),
    });

    if (!response.ok) {
        throw new Error(`Google Places request failed (${response.status})`);
    }

    return (await response.json()) as T;
}

export function newSessionToken(): string {
    return crypto.randomUUID();
}

export type PlaceSuggestion = {
    id: string;
    text: string;
    main: string;
    secondary: string;
};

export async function autocompletePlaces(
    input: string,
    sessionToken: string,
): Promise<PlaceSuggestion[]> {
    const { suggestions = [] } = await placesRequest<{
        suggestions?: {
            placePrediction?: {
                placeId: string;
                text: { text: string };
                structuredFormat?: {
                    mainText?: { text: string };
                    secondaryText?: { text: string };
                };
            };
        }[];
    }>(
        'places:autocomplete',
        'suggestions.placePrediction.placeId,suggestions.placePrediction.text.text,suggestions.placePrediction.structuredFormat',
        { input, sessionToken },
    );

    return suggestions.flatMap(({ placePrediction }) =>
        placePrediction
            ? [
                  {
                      id: placePrediction.placeId,
                      text: placePrediction.text.text,
                      main:
                          placePrediction.structuredFormat?.mainText?.text ??
                          placePrediction.text.text,
                      secondary:
                          placePrediction.structuredFormat?.secondaryText
                              ?.text ?? '',
                  },
              ]
            : [],
    );
}

export async function placeLocation(
    placeId: string,
    sessionToken: string,
): Promise<(LatLng & { address: string }) | null> {
    const place = await placesRequest<{
        location?: { latitude: number; longitude: number };
        formattedAddress?: string;
    }>(
        `places/${encodeURIComponent(placeId)}?sessionToken=${sessionToken}`,
        'location,formattedAddress',
    );

    return place.location
        ? {
              lat: place.location.latitude,
              lng: place.location.longitude,
              address: place.formattedAddress ?? '',
          }
        : null;
}

export async function reverseGeocode(
    lat: number,
    lng: number,
): Promise<{ address: string; placeId: string } | null> {
    await loadScript();

    const { Geocoder } = (await google.maps.importLibrary(
        'geocoding',
    )) as google.maps.GeocodingLibrary;
    const { results } = await new Geocoder().geocode({
        location: { lat, lng },
    });

    return results[0]
        ? {
              address: results[0].formatted_address,
              placeId: results[0].place_id,
          }
        : null;
}

/** Resolve a typed or prefilled address to coordinates for map previews. */
export async function geocodeAddress(
    address: string,
): Promise<(LatLng & { formattedAddress: string }) | null> {
    if (!address.trim()) {
        return null;
    }

    await loadScript();

    const { Geocoder } = (await google.maps.importLibrary(
        'geocoding',
    )) as google.maps.GeocodingLibrary;
    const { results } = await new Geocoder().geocode({ address });
    const result = results[0];
    const location = result?.geometry.location;

    return location
        ? {
              lat: location.lat(),
              lng: location.lng(),
              formattedAddress: result.formatted_address,
          }
        : null;
}

export const mapId =
    (import.meta.env.VITE_GOOGLE_MAPS_MAP_ID as string | undefined) ||
    'DEMO_MAP_ID';

export async function loadMaps(): Promise<{
    maps: google.maps.MapsLibrary;
    marker: google.maps.MarkerLibrary;
}> {
    await loadScript();

    const [maps, marker] = await Promise.all([
        google.maps.importLibrary('maps') as Promise<google.maps.MapsLibrary>,
        google.maps.importLibrary(
            'marker',
        ) as Promise<google.maps.MarkerLibrary>,
    ]);

    return { maps, marker };
}

export type NearbyHub = {
    id: string;
    name: string;
    address: string;
    lat: number;
    lng: number;
    type: 'airport' | 'station';
    photoName: string | null;
};

export async function nearbyHubs(
    lat: number,
    lng: number,
): Promise<NearbyHub[]> {
    const { places = [] } = await placesRequest<{
        places?: {
            id: string;
            displayName?: { text: string };
            formattedAddress?: string;
            location?: { latitude: number; longitude: number };
            types?: string[];
            photos?: { name: string }[];
        }[];
    }>(
        'places:searchNearby',
        'places.id,places.displayName,places.formattedAddress,places.location,places.types,places.photos',
        {
            locationRestriction: {
                circle: {
                    center: { latitude: lat, longitude: lng },
                    radius: 50000,
                },
            },
            includedPrimaryTypes: [
                'airport',
                'international_airport',
                'train_station',
            ],
            rankPreference: 'POPULARITY',
            maxResultCount: 4,
        },
    );

    return places.flatMap((place) =>
        place.location
            ? [
                  {
                      id: place.id,
                      name: place.displayName?.text ?? '',
                      address: place.formattedAddress ?? '',
                      lat: place.location.latitude,
                      lng: place.location.longitude,
                      type: place.types?.some((type) =>
                          type.includes('airport'),
                      )
                          ? 'airport'
                          : 'station',
                      photoName: place.photos?.[0]?.name ?? null,
                  },
              ]
            : [],
    );
}

export type PopularPlace = {
    id: string;
    name: string;
    address: string;
    lat: number;
    lng: number;
    rating: number | null;
    ratingCount: number | null;
    photoName: string | null;
    category: string;
};

export async function nearbyPopularPlaces(
    lat: number,
    lng: number,
): Promise<PopularPlace[]> {
    const { places = [] } = await placesRequest<{
        places?: {
            id: string;
            displayName?: { text: string };
            formattedAddress?: string;
            location?: { latitude: number; longitude: number };
            rating?: number;
            userRatingCount?: number;
            photos?: { name: string }[];
            primaryTypeDisplayName?: { text: string };
        }[];
    }>(
        'places:searchNearby',
        'places.id,places.displayName,places.formattedAddress,places.location,places.rating,places.userRatingCount,places.photos,places.primaryTypeDisplayName',
        {
            locationRestriction: {
                circle: {
                    center: { latitude: lat, longitude: lng },
                    radius: 25000,
                },
            },
            includedPrimaryTypes: [
                'tourist_attraction',
                'museum',
                'park',
                'art_gallery',
                'historical_landmark',
                'shopping_mall',
            ],
            rankPreference: 'POPULARITY',
            maxResultCount: 8,
        },
    );

    return places.flatMap((place) =>
        place.location
            ? [{
                  id: place.id,
                  name: place.displayName?.text ?? 'Popular place',
                  address: place.formattedAddress ?? '',
                  lat: place.location.latitude,
                  lng: place.location.longitude,
                  rating: place.rating ?? null,
                  ratingCount: place.userRatingCount ?? null,
                  photoName: place.photos?.[0]?.name ?? null,
                  category: place.primaryTypeDisplayName?.text ?? 'Attraction',
              }]
            : [],
    );
}

export function placePhotoUrl(photoName: string | null): string | null {
    if (!photoName || !placesKey) return null;

    return `https://places.googleapis.com/v1/${photoName}/media?maxWidthPx=900&key=${encodeURIComponent(placesKey)}`;
}

/** Get the current primary Google Maps photo for a place. */
export async function fetchPlacePhotoUrl(
    placeId: string | null,
    photoName: string | null = null,
): Promise<string | null> {
    if (!placeId) return null;

    try {
        if (apiKey) {
            await loadScript();

            const { Place } = (await google.maps.importLibrary(
                'places',
            )) as google.maps.PlacesLibrary;
            const place = new Place({ id: placeId.replace(/^places\//, '') });

            await place.fetchFields({ fields: ['photos'] });

            const photoUri = place.photos?.[0]?.getURI({ maxWidth: 900 });

            if (photoUri) return photoUri;
        }
    } catch {
        // Fall through to the Places API photo media endpoint.
    }

    if (!photoName || !placesKey) return null;

    try {
        const response = await fetch(
            `https://places.googleapis.com/v1/${photoName}/media?maxWidthPx=900&skipHttpRedirect=true&key=${encodeURIComponent(placesKey)}`,
        );
        const data = (await response.json()) as { photoUri?: string };

        return response.ok ? data.photoUri ?? null : null;
    } catch {
        return null;
    }
}

function straightLineDistanceKm(origin: LatLng, destination: LatLng): number {
    const earthRadius = 6371;
    const radians = (value: number) => (value * Math.PI) / 180;
    const latitudeDelta = radians(destination.lat - origin.lat);
    const longitudeDelta = radians(destination.lng - origin.lng);
    const a = Math.sin(latitudeDelta / 2) ** 2 +
        Math.cos(radians(origin.lat)) * Math.cos(radians(destination.lat)) *
        Math.sin(longitudeDelta / 2) ** 2;

    return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

export type RouteMetrics = {
    distance: string;
    duration: string;
    distanceKm: number;
};

export async function routeMetrics(origin: LatLng, destination: LatLng): Promise<RouteMetrics> {
    const fallbackKm = straightLineDistanceKm(origin, destination);

    try {
        if (!placesKey) throw new Error('Routes API key is missing');

        const response = await fetch('https://routes.googleapis.com/directions/v2:computeRoutes', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Goog-Api-Key': placesKey,
                'X-Goog-FieldMask': 'routes.distanceMeters,routes.duration',
            },
            body: JSON.stringify({
                origin: { location: { latLng: { latitude: origin.lat, longitude: origin.lng } } },
                destination: { location: { latLng: { latitude: destination.lat, longitude: destination.lng } } },
                travelMode: 'DRIVE',
                routingPreference: 'TRAFFIC_AWARE',
            }),
        });

        if (!response.ok) throw new Error('Route calculation failed');

        const route = ((await response.json()) as { routes?: { distanceMeters?: number; duration?: string }[] }).routes?.[0];
        if (!route?.distanceMeters) throw new Error('No route found');

        const distanceKm = route.distanceMeters / 1000;
        const seconds = Number.parseInt(route.duration?.replace('s', '') ?? '', 10);
        const minutes = Number.isFinite(seconds) ? Math.max(1, Math.round(seconds / 60)) : null;

        return {
            distance: `${distanceKm < 10 ? distanceKm.toFixed(1) : Math.round(distanceKm)} km`,
            duration: minutes ? `${minutes} min drive` : 'Driving route',
            distanceKm,
        };
    } catch {
        return {
            distance: `${Math.round(fallbackKm)} km away`,
            duration: 'Distance estimate',
            distanceKm: fallbackKm,
        };
    }
}
