import * as Location from "expo-location";
import { useCallback, useEffect, useState } from "react";

import { useLocationStore } from "@/store";

export type LocationStatus =
  /** Still asking for permission or waiting on the first fix. */
  | "pending"
  /** Coordinates are in the store. */
  | "granted"
  /** The user said no to the permission prompt. */
  | "denied"
  /** Permission is fine, but location is switched off device-wide. */
  | "services-off"
  /** Permission is fine and services are on, but no fix arrived. */
  | "unavailable";

// getCurrentPositionAsync has no timeout of its own: indoors, or on an
// emulator with no mock location set, it waits for a GPS fix that never
// arrives and the screen sits on a spinner forever. This is the cap.
const FIX_TIMEOUT_MS = 15_000;

const LAST_KNOWN_MAX_AGE_MS = 5 * 60 * 1000;

const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T | null> =>
  Promise.race([
    promise,
    new Promise<null>((resolve) => {
      setTimeout(() => resolve(null), ms);
    }),
  ]);

/**
 * Resolves the rider's position into the location store.
 *
 * Takes the fastest usable fix rather than the best one: a cached position
 * renders the map immediately, and a precise reading replaces it when (or if)
 * it arrives. Reverse geocoding is fired separately so a failed lookup costs
 * the address label, never the coordinates.
 */
export const useUserLocation = () => {
  const setUserLocation = useLocationStore((state) => state.setUserLocation);
  const [status, setStatus] = useState<LocationStatus>("pending");
  const [attempt, setAttempt] = useState(0);

  const retry = useCallback(() => setAttempt((count) => count + 1), []);

  useEffect(() => {
    let cancelled = false;

    const apply = ({ coords }: Location.LocationObject) => {
      const { latitude, longitude } = coords;

      setUserLocation({ latitude, longitude, address: "Your location" });

      Location.reverseGeocodeAsync({ latitude, longitude })
        .then(([place]) => {
          if (cancelled || !place) return;

          const address = [place.name, place.city ?? place.region]
            .filter(Boolean)
            .join(", ");

          if (address) setUserLocation({ latitude, longitude, address });
        })
        .catch((error) => console.log("[REVERSE_GEOCODE]: ", error));
    };

    const resolve = async () => {
      setStatus("pending");

      try {
        const { status: permission } =
          await Location.requestForegroundPermissionsAsync();

        if (cancelled) return;

        if (permission !== "granted") {
          setStatus("denied");
          return;
        }

        // Granting the app permission doesn't help if the device radio is off,
        // and getCurrentPositionAsync throws rather than saying so clearly.
        if (!(await Location.hasServicesEnabledAsync())) {
          if (!cancelled) setStatus("services-off");
          return;
        }

        const cached = await Location.getLastKnownPositionAsync({
          maxAge: LAST_KNOWN_MAX_AGE_MS,
        });

        if (cancelled) return;

        if (cached) {
          apply(cached);
          setStatus("granted");
        }

        const fresh = await withTimeout(
          Location.getCurrentPositionAsync({
            accuracy: Location.Accuracy.Balanced,
          }),
          FIX_TIMEOUT_MS,
        );

        if (cancelled) return;

        if (fresh) {
          apply(fresh);
          setStatus("granted");
        } else if (!cached) {
          setStatus("unavailable");
        }
      } catch (error) {
        console.log("[LOCATION]: ", error);

        // A cached fix already on screen is better than an error panel.
        if (!cancelled) {
          setStatus((current) =>
            current === "granted" ? current : "unavailable",
          );
        }
      }
    };

    void resolve();

    return () => {
      cancelled = true;
    };
  }, [attempt, setUserLocation]);

  return { status, retry };
};
