import { MaterialCommunityIcons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
import {
  ActivityIndicator,
  Alert,
  Image,
  Linking,
  ScrollView,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from "react-native";
import ReactNativeModal from "react-native-modal";
import { SafeAreaView } from "react-native-safe-area-context";

import { driverPhotoUri } from "@/lib/driver-photo";
import { CancelSheet } from "@/components/cancel-sheet";
import { CustomButton } from "@/components/custom-button";
import {
  DocumentScanner,
  type DocumentType,
  type ScannedFields,
} from "@/components/document-scanner";
import { Map } from "@/components/map";
import { PickupCodeSheet } from "@/components/pickup-code-sheet";
import { ProfilePhotoPicker } from "@/components/profile-photo-picker";
import { RatingSheet } from "@/components/rating-sheet";
import { icons, images } from "@/constants";
import { REQUEST_TTL_SECONDS } from "@/constants/dispatch";
import { SERVICES, type ServiceId } from "@/constants/services";
import { ApiError, fetchAPI } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import {
  ensureNotificationPermission,
  registerForPush,
} from "@/lib/notifications";
import { useSession } from "@/lib/session";
import { useTheme } from "@/lib/theme";
import { ensureMicPermission } from "@/lib/use-call";
import { useDriverLocation } from "@/lib/use-driver-location";
import { formatTime, haversine } from "@/lib/utils";

// Poll cadence for the driver dashboard (offers / active ride / earnings).
const POLL_MS = 4000;

type ApprovalStatus = "pending" | "approved" | "rejected" | "suspended";

type Profile = {
  id: number;
  first_name: string;
  last_name: string;
  profile_image_url: string | null;
  car_image_url: string | null;
  car_seats: number;
  rating: number;
  rating_count: number;
  service: ServiceId;
  online: boolean;
  car_model: string | null;
  approval_status: ApprovalStatus;
  rejection_reason: string | null;
  license_number: string | null;
  license_expiry: string | null;
  plate_number: string | null;
  /** Stored scan names, or null where nothing has been uploaded. */
  license_image_url: string | null;
  id_image_url: string | null;
  vehicle_reg_image_url: string | null;
};

// The credentials a driver submits for vetting: the numbers an owner checks a
// driver against before letting them near a rider. They are normally read off
// a scan rather than typed, but the driver owns every value in the end — the
// scan prefills the form, it does not submit it.
type Credentials = {
  license_number: string;
  license_expiry: string;
  national_id: string;
  plate_number: string;
};

const CREDENTIAL_KEYS = [
  "license_number",
  "license_expiry",
  "national_id",
  "plate_number",
] as const;

/**
 * Credential fields the resubmit form seeds from the rejected profile. They
 * are treated as replaceable by a fresh scan — see CredentialCapture's
 * `prefilled`. national_id is not among them: the API never sends it back.
 */
const PREFILLED_ON_RESUBMIT = [
  "license_number",
  "license_expiry",
  "plate_number",
] as const satisfies readonly (keyof Credentials)[];

const EMPTY_CREDENTIALS: Credentials = {
  license_number: "",
  license_expiry: "",
  national_id: "",
  plate_number: "",
};

// The stored scans that go up with a submission, keyed the way the API expects
// them. Null means "nothing scanned in this session" — which on a resubmission
// is different from "nothing on file", since the profile may already have one.
type DocumentRefs = {
  license_document: string | null;
  id_document: string | null;
  vehicle_reg_document: string | null;
};

const EMPTY_DOCUMENTS: DocumentRefs = {
  license_document: null,
  id_document: null,
  vehicle_reg_document: null,
};

const DOCUMENT_KEY: Record<DocumentType, keyof DocumentRefs> = {
  license: "license_document",
  id: "id_document",
  vehicle_reg: "vehicle_reg_document",
};

// An open request on the board. Under the broadcast model this is not
// addressed to this driver — it is a job several of them can see and any of
// them can volunteer for, which is why it carries how many have already
// offered and whether this driver is one of them.
type OpenRequest = {
  ride_id: number;
  created_at: string;
  origin_address: string;
  destination_address: string;
  origin_latitude: number;
  origin_longitude: number;
  ride_time: number;
  fare_price: number;
  /** What the driver keeps after the platform fee. */
  payout_cents: number;
  service: string;
  rider_name: string | null;
  rider_rating: number | null;
  /** This driver's live offer on it, or null if they haven't offered. */
  my_offer_id: number | null;
  /** How many drivers are in the running, this one included. */
  offer_count: number;
  /** Metres from this driver's last position to the pickup. */
  pickup_distance_m: number;
};

type ActiveRide = {
  ride_id: number;
  status: string;
  service: string;
  payment_status: string;
  origin_address: string;
  destination_address: string;
  origin_latitude: number;
  origin_longitude: number;
  destination_latitude: number;
  destination_longitude: number;
  ride_time: number;
  fare_price: number;
  /** What the driver keeps after the platform fee. */
  payout_cents: number;
  arrived_at: string | null;
  rider_name: string | null;
  rider_rating: number | null;
};

type Dashboard = {
  now: string;
  requests: OpenRequest[];
  active: ActiveRide | null;
  recent: { ride_id: number; fare_price: number; service: string }[];
  earnings: number;
  platform_fees: number;
  cash_collected: number;
  cash_owed: number;
  /** Cash commission this driver is holding on the company's behalf. */
  owes_company: number;
  /** Card payouts the company still owes this driver. */
  owed_to_driver: number;
  pending_rating: { ride_id: number; rider_name: string | null } | null;
};

const DriverHome = () => {
  const { signOut, user } = useSession();
  const { isDark } = useTheme();
  const t = useT();
  const [loading, setLoading] = useState(true);
  const [profile, setProfile] = useState<Profile | null>(null);
  const [online, setOnline] = useState(false);
  const [dashboard, setDashboard] = useState<Dashboard | null>(null);
  const [busy, setBusy] = useState(false);
  const [codeOpen, setCodeOpen] = useState(false);
  const [codeError, setCodeError] = useState<string | null>(null);
  const [cancelOpen, setCancelOpen] = useState(false);
  // Rating prompts are dismissible; remember which rides were dismissed this
  // session so the next poll doesn't re-open the sheet the driver just closed.
  const [ratingSkipped, setRatingSkipped] = useState<number[]>([]);
  // Retaking the profile photo. A driver whose photo turned out dark or
  // half-cropped is the one rider-facing detail they can't otherwise fix.
  const [photoOpen, setPhotoOpen] = useState(false);

  const loadProfile = useCallback(async () => {
    try {
      const res = await fetchAPI("/(api)/driver/profile");
      const p = res.data as Profile;
      setProfile(p);
      setOnline(p.online);
    } catch (err) {
      // 403 with code ONBOARD means no profile yet — show the onboarding form.
      if (err instanceof ApiError && err.status === 403) {
        setProfile(null);
      } else {
        console.log("[DRIVER_PROFILE_LOAD]: ", err);
      }
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    void loadProfile();
  }, [loadProfile]);

  // Keep the location heartbeat running only while the driver is online and
  // has completed onboarding. Also gives us the driver's own last-known
  // position, so the active-ride card can show it next to the rider's pickup.
  const driverCoords = useDriverLocation(online && profile !== null);

  // Poll the dashboard while online. useCallback keeps the fetcher stable so the
  // interval effect doesn't re-subscribe on every render.
  // Difference between the server's clock and this phone's, refreshed on every
  // poll. The offer countdown is drawn against server time because that's the
  // clock dispatch expires offers on — a phone a few seconds out would
  // otherwise show a timer that runs out early or lingers past the offer.
  const clockOffset = useRef(0);

  // Rides this driver has a live offer on. Offering is a bid, not a booking:
  // the rider may pick someone else and the card simply vanishes on the next
  // poll, which is the most confusing thing that can happen on this screen if
  // nobody says why.
  const myOffers = useRef<number[]>([]);

  const fetchDashboard = useCallback(async () => {
    try {
      const res = await fetchAPI("/(api)/driver/rides");
      const data = res.data as Dashboard;
      if (data.now) clockOffset.current = Date.parse(data.now) - Date.now();

      const stillListed = new Set(data.requests.map((r) => r.ride_id));
      const wonId = data.active?.ride_id ?? null;
      const lost = myOffers.current.filter(
        (rideId) => !stillListed.has(rideId) && rideId !== wonId,
      );
      if (lost.length > 0) {
        Alert.alert(
          t("driver.offerCard.lostTitle"),
          t("driver.offerCard.lostBody"),
        );
      }

      myOffers.current = data.requests
        .filter((r) => r.my_offer_id !== null)
        .map((r) => r.ride_id);

      setDashboard(data);
    } catch (err) {
      console.log("[DRIVER_DASHBOARD_POLL]: ", err);
    }
  }, [t]);

  useEffect(() => {
    if (!online || !profile) return;
    void fetchDashboard();
    const timer = setInterval(() => void fetchDashboard(), POLL_MS);
    return () => clearInterval(timer);
  }, [online, profile, fetchDashboard]);

  // Prime the mic permission as soon as the Message/Call buttons appear on
  // the active-ride card, so the OS prompt lands here instead of mid-handshake
  // after the driver has already tapped Call.
  const activeRideId = dashboard?.active?.ride_id ?? null;
  useEffect(() => {
    if (activeRideId !== null) void ensureMicPermission();
  }, [activeRideId]);

  const toggleOnline = async () => {
    if (!profile) return;
    const next = !online;
    setBusy(true);
    try {
      // Going online is the first moment there's a concrete reason to
      // interrupt this driver, so the notification prompt lands here rather
      // than at app start where it would read as a random demand. Registering
      // for remote push is attempted at the same time; it is a no-op until
      // push credentials are configured.
      if (next) {
        await ensureNotificationPermission();
        void registerForPush();
      }

      await fetchAPI("/(api)/driver/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ online: next }),
      });
      setOnline(next);
      setProfile({ ...profile, online: next });
      if (!next) setDashboard(null);
    } catch (err) {
      console.log("[DRIVER_TOGGLE]: ", err);

      // 403 means the profile lost its approval while the app was open (an
      // owner suspended it). Reload so the review screen takes over rather
      // than leaving the driver tapping a toggle that will never work.
      if (err instanceof ApiError && err.status === 403) {
        await loadProfile();
        return;
      }

      // The server refuses to take a driver offline mid-ride — going dark on a
      // rider who is waiting for you is the one case worth blocking outright.
      Alert.alert(
        t("driver.home.alertErrorTitle"),
        err instanceof ApiError && err.status === 409
          ? t("driver.home.alertOfflineBlocked")
          : t("driver.home.alertToggleBody"),
      );
    } finally {
      setBusy(false);
    }
  };

  // Volunteer for a request, or take the offer back. Neither is an
  // assignment: the rider decides, and until they do this driver stays on the
  // board and free to offer on other jobs.
  const respond = async (
    request: OpenRequest,
    action: "offer" | "withdraw",
  ) => {
    setBusy(true);
    try {
      await fetchAPI(`/(api)/ride/${request.ride_id}/offer`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action }),
      });
      // Don't warn about a request disappearing that this driver just walked
      // away from themselves.
      if (action === "withdraw") {
        myOffers.current = myOffers.current.filter(
          (id) => id !== request.ride_id,
        );
      }
      await fetchDashboard();
    } catch (err) {
      console.log("[DRIVER_OFFER]: ", err);
      // A 409 is the ordinary outcome of several drivers wanting the same job:
      // somebody was picked while this one was reading it.
      if (err instanceof ApiError && err.status === 409) {
        Alert.alert(
          t("driver.offerCard.lostTitle"),
          t("driver.offerCard.lostBody"),
        );
        myOffers.current = myOffers.current.filter(
          (id) => id !== request.ride_id,
        );
        await fetchDashboard();
        return;
      }
      Alert.alert(
        t("driver.activeRide.alertErrorTitle"),
        action === "offer"
          ? t("driver.offerCard.alertOfferBody")
          : t("driver.offerCard.alertWithdrawBody"),
      );
    } finally {
      setBusy(false);
    }
  };

  // Generic state push for transitions the driver can make unilaterally
  // ("I'm at the pickup point", "the trip is done").
  const advance = async (
    rideId: number,
    status: "arrived" | "completed",
    extra?: Record<string, unknown>,
  ) => {
    setBusy(true);
    try {
      await fetchAPI(`/(api)/ride/${rideId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ status, ...extra }),
      });
      await fetchDashboard();
    } catch (err) {
      console.log("[DRIVER_ADVANCE]: ", err);
      Alert.alert(
        t("driver.activeRide.alertErrorTitle"),
        t("driver.activeRide.alertUpdateBody"),
      );
    } finally {
      setBusy(false);
    }
  };

  // Starting the trip needs the rider's pickup code, so it goes through the
  // code sheet rather than a plain state push.
  const startTrip = async (rideId: number, code: string) => {
    setBusy(true);
    setCodeError(null);
    try {
      await fetchAPI(`/(api)/ride/${rideId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ status: "en_route", pickup_code: code }),
      });
      setCodeOpen(false);
      await fetchDashboard();
    } catch (err) {
      console.log("[DRIVER_START_TRIP]: ", err);
      // 403 is specifically "that code doesn't match" — keep the sheet open so
      // the driver can re-read it off the rider's phone and try again.
      setCodeError(
        err instanceof ApiError && err.status === 403
          ? t("pickupCode.wrongCode")
          : t("driver.activeRide.alertUpdateBody"),
      );
    } finally {
      setBusy(false);
    }
  };

  // Completing a cash ride also settles the money: the driver confirms they
  // took the fare, which is what turns it from owed into collected.
  const completeRide = (ride: ActiveRide) => {
    if (ride.payment_status !== "cash") {
      void advance(ride.ride_id, "completed");
      return;
    }

    Alert.alert(
      t("driver.activeRide.cashConfirmTitle"),
      t("driver.activeRide.cashConfirmBody", {
        amount: (ride.fare_price / 100).toFixed(2),
      }),
      [
        // "Not collected" still completes the trip — the rider has been
        // dropped off either way. It leaves the fare marked as owed so it
        // shows up as an unsettled balance instead of vanishing.
        {
          text: t("driver.activeRide.cashNotCollected"),
          onPress: () => void advance(ride.ride_id, "completed"),
        },
        {
          text: t("driver.activeRide.cashCollected"),
          onPress: () =>
            void advance(ride.ride_id, "completed", { cash_collected: true }),
        },
      ],
    );
  };

  // Only allowed before the trip starts ('accepted' / 'arrived'): once en
  // route the driver already has the rider, so aborting is "complete the
  // trip", not "cancel" it.
  const cancelRide = async (rideId: number, reason: string) => {
    setBusy(true);
    try {
      await fetchAPI(`/(api)/ride/${rideId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ status: "cancelled", reason }),
      });
      setCancelOpen(false);
      await fetchDashboard();
    } catch (err) {
      console.log("[DRIVER_CANCEL]: ", err);
      Alert.alert(
        t("driver.activeRide.alertErrorTitle"),
        t("driver.activeRide.alertCancelBody"),
      );
    } finally {
      setBusy(false);
    }
  };

  if (loading) {
    return (
      <SafeAreaView className="flex-1 bg-white dark:bg-neutral-950 items-center justify-center">
        <ActivityIndicator
          size="large"
          color={isDark ? "#0286ff" : "#0286ff"}
        />
      </SafeAreaView>
    );
  }

  if (!profile) {
    return (
      <Onboarding
        onCreated={loadProfile}
        signOut={signOut}
        userName={user?.name}
      />
    );
  }

  // A profile is not a licence to drive. Until an owner has reviewed the
  // driver's credentials the dashboard is replaced by the review screen —
  // there is nothing here they can act on, and the server would refuse the
  // online toggle anyway.
  if (profile.approval_status !== "approved") {
    return (
      <ReviewStatus
        profile={profile}
        onRefresh={loadProfile}
        signOut={signOut}
      />
    );
  }

  const earnings = dashboard?.earnings ?? 0;
  const rideCount = dashboard?.recent.length ?? 0;
  const cashCollected = dashboard?.cash_collected ?? 0;
  const cashOwed = dashboard?.cash_owed ?? 0;
  const platformFees = dashboard?.platform_fees ?? 0;
  const owesCompany = dashboard?.owes_company ?? 0;
  const owedToDriver = dashboard?.owed_to_driver ?? 0;
  const pendingRating = dashboard?.pending_rating ?? null;

  return (
    <SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
      <ScrollView
        className="flex-1 px-5"
        contentContainerStyle={{ paddingBottom: 40 }}
      >
        <View className="flex-row items-center justify-between my-5">
          <View className="flex-row items-center flex-1">
            <TouchableOpacity
              onPress={() => setPhotoOpen(true)}
              className="w-12 h-12 rounded-full bg-white dark:bg-neutral-900 items-center justify-center overflow-hidden mr-3"
            >
              {profile.profile_image_url ? (
                <Image
                  source={{ uri: driverPhotoUri(profile.profile_image_url) }}
                  className="w-12 h-12"
                  resizeMode="cover"
                  alt={t("driver.photo.title")}
                />
              ) : (
                <MaterialCommunityIcons
                  name="camera-plus-outline"
                  size={20}
                  color={isDark ? "#9ca3af" : "#858585"}
                />
              )}
            </TouchableOpacity>
            <View className="flex-1">
              <Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
                {t("driver.home.driverMode")}
              </Text>
              {/* A driver's own rating drives whether they keep working here, and
                it was being fetched but never shown. */}
              <Text className="text-xs font-JakartaMedium text-general-200 dark:text-neutral-400 mt-0.5">
                ★ {Number(profile.rating).toFixed(1)}
                {profile.rating_count > 0
                  ? ` · ${t("driver.home.ratingCount", {
                      n: String(profile.rating_count),
                    })}`
                  : ` · ${t("driver.home.ratingNew")}`}
              </Text>
            </View>
          </View>
          <TouchableOpacity
            onPress={signOut}
            className="w-10 h-10 rounded-full bg-white dark:bg-neutral-900 items-center justify-center"
          >
            <Image
              source={icons.out}
              className="w-4 h-4"
              alt={t("driver.home.signOutAlt")}
            />
          </TouchableOpacity>
        </View>

        {/* Online / offline toggle */}
        <TouchableOpacity
          onPress={toggleOnline}
          disabled={busy}
          className={`rounded-2xl p-5 items-center mb-4 ${
            online ? "bg-emerald-500" : "bg-neutral-700 dark:bg-neutral-800"
          }`}
        >
          <Text className="text-white text-lg font-JakartaBold">
            {online ? t("driver.home.online") : t("driver.home.goOnline")}
          </Text>
        </TouchableOpacity>

        {/* Earnings summary */}
        <View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-4 flex-row justify-between">
          <View>
            <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
              {t("driver.home.todaysEarnings")}
            </Text>
            <Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
              ${(earnings / 100).toFixed(2)}
            </Text>
            {/* Says where the difference went. A driver who charged $20 in
                fares and sees $16 needs the missing $4 accounted for on the
                same screen, or the number reads as an error. */}
            {platformFees > 0 ? (
              <Text className="text-[11px] font-JakartaMedium text-general-200 dark:text-neutral-500 mt-0.5">
                {t("driver.home.afterFee", {
                  fee: (platformFees / 100).toFixed(2),
                })}
              </Text>
            ) : null}
          </View>
          <View className="items-end">
            <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
              {t("driver.home.completedToday")}
            </Text>
            <Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
              {rideCount}
            </Text>
          </View>
        </View>

        {/* Cash in hand. Separated from earnings because it's money the driver
            is holding on the platform's behalf, and the figure they'll be
            reconciled against at the end of the day. */}
        {cashCollected > 0 ? (
          <View className="bg-white dark:bg-neutral-900 rounded-2xl px-4 py-3 mb-4 flex-row justify-between items-center">
            <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
              {t("driver.home.cashInHand")}
            </Text>
            <Text className="font-JakartaBold text-amber-600 dark:text-amber-400">
              ${(cashCollected / 100).toFixed(2)}
            </Text>
          </View>
        ) : null}

        {/* The running balance with the company, both directions. Kept
            separate from today's earnings because it doesn't reset at
            midnight — commission a driver is holding from Tuesday is still
            owed on Friday, and a driver who can't see it has no way to know
            what they'll be asked for. */}
        {owesCompany > 0 || owedToDriver > 0 ? (
          <View className="bg-white dark:bg-neutral-900 rounded-2xl px-4 py-3 mb-4">
            {owesCompany > 0 ? (
              <View className="flex-row justify-between items-center">
                <View className="flex-1 pr-2">
                  <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
                    {t("driver.home.owesCompany")}
                  </Text>
                  <Text className="text-[11px] text-general-200 dark:text-neutral-500 font-Jakarta mt-0.5">
                    {t("driver.home.owesCompanyHint")}
                  </Text>
                </View>
                <Text className="font-JakartaBold text-amber-600 dark:text-amber-400">
                  ${(owesCompany / 100).toFixed(2)}
                </Text>
              </View>
            ) : null}

            {owesCompany > 0 && owedToDriver > 0 ? (
              <View className="border-t border-neutral-100 dark:border-neutral-800 my-2.5" />
            ) : null}

            {owedToDriver > 0 ? (
              <View className="flex-row justify-between items-center">
                <View className="flex-1 pr-2">
                  <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
                    {t("driver.home.owedToDriver")}
                  </Text>
                  <Text className="text-[11px] text-general-200 dark:text-neutral-500 font-Jakarta mt-0.5">
                    {t("driver.home.owedToDriverHint")}
                  </Text>
                </View>
                <Text className="font-JakartaBold text-emerald-600 dark:text-emerald-400">
                  ${(owedToDriver / 100).toFixed(2)}
                </Text>
              </View>
            ) : null}
          </View>
        ) : null}

        {/* Fares that were never collected. These no longer count towards the
            earnings headline, so they're shown here instead of silently
            inflating a number the driver won't be paid. */}
        {cashOwed > 0 ? (
          <View className="bg-white dark:bg-neutral-900 rounded-2xl px-4 py-3 mb-4 flex-row justify-between items-center">
            <Text className="text-general-200 dark:text-neutral-400 text-xs font-JakartaMedium">
              {t("driver.home.uncollected")}
            </Text>
            <Text className="font-JakartaBold text-rose-500">
              ${(cashOwed / 100).toFixed(2)}
            </Text>
          </View>
        ) : null}

        {/* Active ride */}
        {dashboard?.active ? (
          <ActiveRideCard
            ride={dashboard.active}
            busy={busy}
            onArrived={(rideId) => void advance(rideId, "arrived")}
            onStartTrip={() => {
              setCodeError(null);
              setCodeOpen(true);
            }}
            onComplete={completeRide}
            onCancel={() => setCancelOpen(true)}
            driverCoords={driverCoords}
          />
        ) : null}

        {/* The board: open requests near this driver, newest and nearest
            first. Hidden while they're on a ride — a driver mid-trip taking a
            second job is the one thing this screen must not make easy. */}
        <Text className="text-xl font-JakartaBold mt-4 mb-3 text-black dark:text-white">
          {online
            ? t("driver.home.incomingRequests")
            : t("driver.home.incomingRequestsOffline")}
        </Text>

        {!online || dashboard?.active ? null : dashboard?.requests.length ? (
          dashboard.requests.map((request) => (
            <RequestCard
              key={request.ride_id}
              request={request}
              busy={busy}
              clockOffset={clockOffset.current}
              onOffer={() => respond(request, "offer")}
              onWithdraw={() => respond(request, "withdraw")}
            />
          ))
        ) : (
          <View className="bg-white dark:bg-neutral-900 rounded-2xl p-6 items-center">
            <Image
              source={images.noResult}
              className="w-24 h-24"
              resizeMode="contain"
            />
            <Text className="text-general-200 dark:text-neutral-400 mt-2">
              {!online
                ? t("driver.home.goOnlineStart")
                : dashboard?.active
                  ? t("driver.home.finishCurrentRide")
                  : t("driver.home.waitingRequests")}
            </Text>
          </View>
        )}
      </ScrollView>

      {dashboard?.active ? (
        <>
          <PickupCodeSheet
            visible={codeOpen}
            submitting={busy}
            error={codeError}
            onCancel={() => setCodeOpen(false)}
            onSubmit={(code) => void startTrip(dashboard.active!.ride_id, code)}
          />
          <CancelSheet
            visible={cancelOpen}
            audience="driver"
            submitting={busy}
            onCancel={() => setCancelOpen(false)}
            onConfirm={(reason) =>
              void cancelRide(dashboard.active!.ride_id, reason)
            }
          />
        </>
      ) : null}

      {/* Rate the rider once the trip is done. Prompted from the dashboard
          rather than at drop-off so it survives the driver immediately
          accepting their next request. */}
      {pendingRating && !ratingSkipped.includes(pendingRating.ride_id) ? (
        <RatingSheet
          visible
          rideId={pendingRating.ride_id}
          audience="driver"
          subjectName={pendingRating.rider_name}
          onDone={() => {
            setRatingSkipped((prev) => [...prev, pendingRating.ride_id]);
            void fetchDashboard();
          }}
          onSkip={() =>
            setRatingSkipped((prev) => [...prev, pendingRating.ride_id])
          }
        />
      ) : null}

      {/* Retaking the profile photo. The upload attaches itself server-side
          for a driver who already has a profile, so all this has to do
          afterwards is reload the profile and let the new photo show. */}
      <ReactNativeModal
        isVisible={photoOpen}
        onBackdropPress={() => setPhotoOpen(false)}
      >
        <View className="bg-white dark:bg-neutral-900 p-6 rounded-2xl">
          <ProfilePhotoPicker
            current={profile.profile_image_url}
            onUploaded={() => void loadProfile()}
          />
          <CustomButton
            title={t("common.save")}
            onPress={() => setPhotoOpen(false)}
          />
        </View>
      </ReactNativeModal>
    </SafeAreaView>
  );
};

// --- Credentials --------------------------------------------------------

// A labelled text input matching the onboarding form's styling. Pulled out
// because onboarding and the resubmit-after-rejection flow collect the same
// four fields and must not drift apart.
const Field = ({
  label,
  value,
  onChange,
  placeholder,
  keyboardType,
  autoCapitalize = "characters",
}: {
  label: string;
  value: string;
  onChange: (next: string) => void;
  placeholder: string;
  keyboardType?: "default" | "number-pad" | "numbers-and-punctuation";
  autoCapitalize?: "none" | "characters" | "words";
}) => {
  const { isDark } = useTheme();

  return (
    <>
      <Text className="text-sm font-JakartaSemiBold mb-2 text-black dark:text-white">
        {label}
      </Text>
      <TextInput
        value={value}
        onChangeText={onChange}
        placeholder={placeholder}
        placeholderTextColor={isDark ? "#737373" : "#858585"}
        keyboardType={keyboardType}
        autoCapitalize={autoCapitalize}
        autoCorrect={false}
        className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
      />
    </>
  );
};

const CredentialFields = ({
  values,
  onChange,
  only,
}: {
  values: Credentials;
  onChange: (next: Credentials) => void;
  /**
   * Render just these fields. Used to ask for the one value a scan couldn't
   * read without putting the three it did read back in front of the driver as
   * a form to re-check.
   */
  only?: readonly (keyof Credentials)[];
}) => {
  const t = useT();
  const set = (key: keyof Credentials) => (next: string) =>
    onChange({ ...values, [key]: next });
  const show = (key: keyof Credentials) => !only || only.includes(key);

  return (
    <>
      {show("license_number") && (
        <Field
          label={t("driver.credentials.licenseNumber")}
          value={values.license_number}
          onChange={set("license_number")}
          placeholder={t("driver.credentials.licenseNumberPlaceholder")}
        />
      )}
      {show("license_expiry") && (
        <Field
          label={t("driver.credentials.licenseExpiry")}
          value={values.license_expiry}
          onChange={set("license_expiry")}
          placeholder="YYYY-MM-DD"
          keyboardType="numbers-and-punctuation"
          autoCapitalize="none"
        />
      )}
      {show("national_id") && (
        <Field
          label={t("driver.credentials.nationalId")}
          value={values.national_id}
          onChange={set("national_id")}
          placeholder={t("driver.credentials.nationalIdPlaceholder")}
        />
      )}
      {show("plate_number") && (
        <Field
          label={t("driver.credentials.plateNumber")}
          value={values.plate_number}
          onChange={set("plate_number")}
          placeholder={t("driver.credentials.plateNumberPlaceholder")}
        />
      )}
    </>
  );
};

/**
 * What the scans read, shown back as a receipt rather than a form.
 *
 * The driver is not asked to check these — a human reviewer does that against
 * the stored scan before the profile is ever approved. This is here so the
 * driver can see that something was actually read off their documents, and
 * spot a wrong number if one happens to catch their eye.
 */
const CredentialSummary = ({ values }: { values: Credentials }) => {
  const t = useT();

  const rows: [string, string][] = [
    [t("driver.credentials.licenseNumber"), values.license_number],
    [t("driver.credentials.licenseExpiry"), values.license_expiry],
    [t("driver.credentials.nationalId"), values.national_id],
    [t("driver.credentials.plateNumber"), values.plate_number],
  ];

  return (
    <View className="bg-emerald-500/10 border border-emerald-500 rounded-2xl p-4 mb-4">
      <View className="flex-row items-center mb-3">
        <MaterialCommunityIcons
          name="check-circle-outline"
          size={16}
          color="#10b981"
        />
        <Text className="text-sm font-JakartaBold text-emerald-600 dark:text-emerald-400 ml-1.5">
          {t("driver.scan.allRead")}
        </Text>
      </View>

      {rows.map(([label, value]) => (
        <View key={label} className="flex-row justify-between py-1">
          <Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400 flex-1 pr-2">
            {label}
          </Text>
          <Text className="text-xs font-JakartaBold text-black dark:text-white">
            {value}
          </Text>
        </View>
      ))}
    </View>
  );
};

/**
 * The three document scanners, and the credential fields they usually make
 * unnecessary.
 *
 * The driver's job here is to photograph their documents — not to transcribe
 * them and not to proof-read a form. So when a scan yields everything, no
 * inputs are rendered at all: the values are shown back as a receipt and the
 * driver submits. Typing only ever appears for what a scan genuinely could not
 * read, which is the one case where hiding the field would leave the driver
 * stuck with no way forward.
 *
 * What makes that safe is that nothing downstream trusts a scanned value:
 * an owner reviews every profile against the stored image before it can take a
 * ride, so a misread is caught by a person rather than by asking every driver
 * to check every field on the off-chance.
 *
 * Onboarding and the resubmit-after-rejection flow both use this, which is
 * what keeps the two paths from drifting — a rejected driver re-scans exactly
 * the documents a new one scans.
 *
 * The merge rule is the other half. A scanned value is written into a field
 * that is empty, or into one an earlier scan filled; a value the driver typed
 * themselves is never overwritten. Without that, correcting a misread expiry
 * and then re-scanning a blurry ID card would quietly stamp the correction
 * back out — and the driver would submit a number they had already fixed once.
 */
const CredentialCapture = ({
  values,
  onChange,
  documents,
  onDocuments,
  onFile,
  prefilled,
  onCarModel,
}: {
  values: Credentials;
  onChange: (next: Credentials) => void;
  documents: DocumentRefs;
  onDocuments: (next: DocumentRefs) => void;
  /** Documents already stored on the profile, for the resubmission flow. */
  onFile?: Partial<Record<DocumentType, boolean>>;
  /**
   * Fields whose starting value came from an earlier submission rather than
   * from the driver typing it now. A resubmission is usually a rejection over
   * exactly one of those values, so a rescan has to be allowed to replace
   * them — otherwise re-photographing the licence leaves the wrong number the
   * reviewer already rejected sitting in the form.
   */
  prefilled?: readonly (keyof Credentials)[];
  /** Called when a vehicle registration yields a car model. */
  onCarModel?: (model: string) => void;
}) => {
  const t = useT();
  const [autofilled, setAutofilled] = useState<Set<keyof Credentials>>(
    () => new Set(prefilled),
  );

  // A scan is on file already when resubmitting, so the summary and the
  // "needed" set have to reflect the values that came back with the profile —
  // not wait for a rescan that the driver may not have to make.
  const hasScan =
    documents.license_document !== null || Boolean(onFile?.license);

  /**
   * Fields a scan left empty, frozen at the moment the scan completed rather
   * than derived from the current values. Deriving it live would make each
   * input vanish the instant the driver typed the first character into it.
   */
  const [needed, setNeeded] = useState<readonly (keyof Credentials)[]>(() =>
    hasScan ? CREDENTIAL_KEYS.filter((key) => !values[key].trim()) : [],
  );

  // Opened by the driver when they spot a wrong value. Never the default:
  // the point of scanning is that there is no form to work through.
  const [editing, setEditing] = useState(false);

  const applyScan = (
    docType: DocumentType,
    document: string,
    fields: ScannedFields,
  ) => {
    onDocuments({ ...documents, [DOCUMENT_KEY[docType]]: document });

    const next = { ...values };
    const filled = new Set(autofilled);

    for (const field of CREDENTIAL_KEYS) {
      const scanned = fields[field]?.trim();
      if (!scanned) continue;
      if (next[field].trim() && !filled.has(field)) continue;

      next[field] = scanned;
      filled.add(field);
    }

    setAutofilled(filled);
    setNeeded(CREDENTIAL_KEYS.filter((key) => !next[key].trim()));
    onChange(next);

    if (fields.car_model) onCarModel?.(fields.car_model);
  };

  const scanners: { docType: DocumentType; optional?: boolean }[] = [
    { docType: "license" },
    { docType: "id", optional: true },
    { docType: "vehicle_reg", optional: true },
  ];

  return (
    <>
      {scanners.map(({ docType, optional }) => (
        <DocumentScanner
          key={docType}
          docType={docType}
          label={t(`driver.scan.${docType}Label`)}
          hint={t(`driver.scan.${docType}Hint`)}
          optional={optional}
          onFile={onFile?.[docType] ?? false}
          onScanned={(document, fields) => applyScan(docType, document, fields)}
        />
      ))}

      {/* Everything read: no form, just what we got. */}
      {hasScan && needed.length === 0 && !editing ? (
        <CredentialSummary values={values} />
      ) : null}

      {/* Something didn't read, or the driver opened the details to fix a
          value. Only the unreadable fields are asked for — the ones that
          scanned cleanly stay out of the way unless editing is open. */}
      {needed.length > 0 || editing ? (
        <>
          <Text className="text-sm text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
            {editing
              ? t("driver.scan.checkPrompt")
              : t("driver.scan.missingPrompt")}
          </Text>

          <CredentialFields
            values={values}
            onChange={onChange}
            only={editing ? undefined : needed}
          />
        </>
      ) : null}

      {/* An escape hatch for a driver who spots a wrong digit, deliberately
          understated so it doesn't read as a step they have to complete. */}
      {hasScan ? (
        <TouchableOpacity
          onPress={() => setEditing((open) => !open)}
          className="self-start mb-4"
        >
          <Text className="text-sm font-JakartaBold text-primary-500">
            {editing ? t("driver.scan.done") : t("driver.scan.edit")}
          </Text>
        </TouchableOpacity>
      ) : null}
    </>
  );
};

// Client-side mirror of the server's checks, so an obvious mistake is caught
// before a round trip. The server re-validates regardless.
const credentialError = (
  values: Credentials,
  hasLicenseScan: boolean,
): string | null => {
  if (!hasLicenseScan) return "driver.credentials.errorScanRequired";

  if (
    !values.license_number.trim() ||
    !values.national_id.trim() ||
    !values.plate_number.trim()
  ) {
    return "driver.credentials.errorMissing";
  }

  if (!/^\d{4}-\d{2}-\d{2}$/.test(values.license_expiry.trim())) {
    return "driver.credentials.errorExpiryFormat";
  }

  const expiry = new Date(`${values.license_expiry.trim()}T00:00:00Z`);
  if (Number.isNaN(expiry.getTime()) || expiry.getTime() <= Date.now()) {
    return "driver.credentials.errorExpired";
  }

  return null;
};

// --- Onboarding form ------------------------------------------------------

const Onboarding = ({
  onCreated,
  signOut,
  userName,
}: {
  onCreated: () => Promise<void>;
  signOut: () => Promise<void>;
  userName?: string | null;
}) => {
  const t = useT();
  const { isDark } = useTheme();
  const [service, setService] = useState<ServiceId>("car");
  const [carModel, setCarModel] = useState("");
  const [carSeats, setCarSeats] = useState("4");
  const [credentials, setCredentials] =
    useState<Credentials>(EMPTY_CREDENTIALS);
  const [documents, setDocuments] = useState<DocumentRefs>(EMPTY_DOCUMENTS);
  const [photo, setPhoto] = useState<string | null>(null);
  const [submitting, setSubmitting] = useState(false);

  const submit = async () => {
    if (!photo) {
      Alert.alert(
        t("driver.credentials.errorTitle"),
        t("driver.photo.required"),
      );
      return;
    }

    const seats = Number(carSeats);
    if (!Number.isInteger(seats) || seats < 1 || seats > 8) {
      Alert.alert(
        t("driver.home.alertSeatsTitle"),
        t("driver.home.alertSeatsBody"),
      );
      return;
    }

    const problem = credentialError(
      credentials,
      documents.license_document !== null,
    );
    if (problem) {
      Alert.alert(t("driver.credentials.errorTitle"), t(problem));
      return;
    }

    setSubmitting(true);
    try {
      await fetchAPI("/(api)/driver/profile", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          service,
          car_model: carModel.trim() || null,
          car_seats: seats,
          license_number: credentials.license_number.trim(),
          license_expiry: credentials.license_expiry.trim(),
          national_id: credentials.national_id.trim(),
          plate_number: credentials.plate_number.trim(),
          profile_photo: photo,
          ...documents,
        }),
      });
      await onCreated();
    } catch (err) {
      console.log("[DRIVER_ONBOARD]: ", err);
      Alert.alert(
        t("driver.home.alertErrorTitle"),
        t("driver.home.alertCreateBody"),
      );
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <SafeAreaView className="flex-1 bg-white dark:bg-neutral-950">
      <ScrollView
        className="flex-1 px-5"
        contentContainerStyle={{ paddingBottom: 40 }}
      >
        <View className="flex-row items-center justify-between my-5">
          <Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
            {t("driver.home.welcome", {
              name: userName?.split(" ")[0] || t("driver.home.welcomeFallback"),
            })}
          </Text>
          <TouchableOpacity
            onPress={signOut}
            className="w-10 h-10 rounded-full bg-neutral-100 dark:bg-neutral-900 items-center justify-center"
          >
            <Image
              source={icons.out}
              className="w-4 h-4"
              alt={t("driver.home.signOutAlt")}
            />
          </TouchableOpacity>
        </View>

        <Text className="text-base text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
          {t("driver.home.setupIntro")}
        </Text>

        {/* First thing in the form, because it is the first thing a rider
            sees: the photo shown beside this driver's name when riders pick
            between offers. */}
        <ProfilePhotoPicker onUploaded={setPhoto} />

        <Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
          {t("driver.home.whatDrive")}
        </Text>
        <View className="flex-row gap-2 mb-5">
          {SERVICES.map((item) => {
            const active = item.id === service;
            return (
              <TouchableOpacity
                key={item.id}
                onPress={() => setService(item.id)}
                className={`flex-1 items-center rounded-2xl border py-3 ${
                  active
                    ? "border-primary-500 bg-primary-500/10"
                    : "border-neutral-100 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900"
                }`}
              >
                <MaterialCommunityIcons
                  name={item.icon}
                  size={24}
                  color={active ? "#0286ff" : isDark ? "#9ca3af" : "#858585"}
                />
                <Text
                  className={`mt-1.5 text-xs font-JakartaBold ${
                    active ? "text-primary-500" : "text-black dark:text-white"
                  }`}
                >
                  {t(item.labelKey)}
                </Text>
              </TouchableOpacity>
            );
          })}
        </View>

        <Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
          {t("driver.home.carModel")}
        </Text>
        <TextInput
          value={carModel}
          onChangeText={setCarModel}
          placeholder={t("driver.home.carModelPlaceholder")}
          placeholderTextColor={isDark ? "#737373" : "#858585"}
          className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-4"
          autoCapitalize="words"
        />

        <Text className="text-lg font-JakartaSemiBold mb-3 text-black dark:text-white">
          {t("driver.home.carSeats")}
        </Text>
        <TextInput
          value={carSeats}
          onChangeText={setCarSeats}
          placeholder={t("driver.home.carSeatsPlaceholder")}
          placeholderTextColor={isDark ? "#737373" : "#858585"}
          keyboardType="number-pad"
          className="bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white rounded-full px-4 py-4 font-JakartaSemiBold text-[15px] mb-8"
        />

        <Text className="text-lg font-JakartaSemiBold mb-1 text-black dark:text-white">
          {t("driver.credentials.title")}
        </Text>
        <Text className="text-sm text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
          {t("driver.credentials.intro")}
        </Text>

        <CredentialCapture
          values={credentials}
          onChange={setCredentials}
          documents={documents}
          onDocuments={setDocuments}
          // The registration names the car, and the driver has almost
          // certainly left the model blank at this point — but never overwrite
          // something they typed themselves.
          onCarModel={(model) => setCarModel((prev) => prev.trim() || model)}
        />

        <Text className="text-xs text-general-200 dark:text-neutral-400 font-Jakarta mb-6 mt-2">
          {t("driver.credentials.reviewNote")}
        </Text>

        <CustomButton
          title={
            submitting ? t("common.saving") : t("driver.credentials.submit")
          }
          onPress={submit}
          disabled={submitting}
        />
      </ScrollView>
    </SafeAreaView>
  );
};

// --- Review status --------------------------------------------------------

// What a driver sees between submitting their credentials and being cleared to
// drive. Pending and suspended are read-only; a rejection is actionable, so it
// carries the owner's reason and a form to correct and resubmit.
const REVIEW_POLL_MS = 20000;

const ReviewStatus = ({
  profile,
  onRefresh,
  signOut,
}: {
  profile: Profile;
  onRefresh: () => Promise<void>;
  signOut: () => Promise<void>;
}) => {
  const t = useT();
  const status = profile.approval_status;
  const [credentials, setCredentials] = useState<Credentials>({
    license_number: profile.license_number ?? "",
    license_expiry: profile.license_expiry
      ? profile.license_expiry.slice(0, 10)
      : "",
    national_id: "",
    plate_number: profile.plate_number ?? "",
  });
  const [documents, setDocuments] = useState<DocumentRefs>(EMPTY_DOCUMENTS);
  const [submitting, setSubmitting] = useState(false);

  // A decision can land at any moment and there's nothing else on this screen
  // to do, so poll rather than making the driver pull to refresh.
  useEffect(() => {
    const timer = setInterval(() => void onRefresh(), REVIEW_POLL_MS);
    return () => clearInterval(timer);
  }, [onRefresh]);

  const resubmit = async () => {
    // A rejection is often about the numbers, not the scan, so a driver may
    // resubmit on the licence photo already on file rather than re-taking it.
    const hasLicenseScan =
      documents.license_document !== null || profile.license_image_url !== null;

    const problem = credentialError(credentials, hasLicenseScan);
    if (problem) {
      Alert.alert(t("driver.credentials.errorTitle"), t(problem));
      return;
    }

    setSubmitting(true);
    try {
      await fetchAPI("/(api)/driver/profile", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          license_number: credentials.license_number.trim(),
          license_expiry: credentials.license_expiry.trim(),
          national_id: credentials.national_id.trim(),
          plate_number: credentials.plate_number.trim(),
          ...documents,
        }),
      });
      await onRefresh();
    } catch (err) {
      console.log("[DRIVER_RESUBMIT]: ", err);
      Alert.alert(
        t("driver.home.alertErrorTitle"),
        t("driver.credentials.alertResubmitBody"),
      );
    } finally {
      setSubmitting(false);
    }
  };

  const tone =
    status === "rejected" || status === "suspended"
      ? {
          badge: "bg-rose-500/10 border-rose-500",
          text: "text-rose-500",
          icon: "alert-circle-outline" as const,
        }
      : {
          badge: "bg-amber-500/10 border-amber-500",
          text: "text-amber-600 dark:text-amber-400",
          icon: "clock-outline" as const,
        };

  return (
    <SafeAreaView className="flex-1 bg-general-500 dark:bg-neutral-950">
      <ScrollView
        className="flex-1 px-5"
        contentContainerStyle={{ paddingBottom: 40 }}
      >
        <View className="flex-row items-center justify-between my-5">
          <Text className="text-2xl font-JakartaExtraBold text-black dark:text-white">
            {t("driver.home.driverMode")}
          </Text>
          <TouchableOpacity
            onPress={signOut}
            className="w-10 h-10 rounded-full bg-white dark:bg-neutral-900 items-center justify-center"
          >
            <Image
              source={icons.out}
              className="w-4 h-4"
              alt={t("driver.home.signOutAlt")}
            />
          </TouchableOpacity>
        </View>

        <View className={`rounded-2xl border p-6 items-center ${tone.badge}`}>
          <MaterialCommunityIcons
            name={tone.icon}
            size={40}
            color={status === "pending" ? "#d97706" : "#f43f5e"}
          />
          <Text
            className={`text-lg font-JakartaBold mt-3 text-center ${tone.text}`}
          >
            {t(`driver.review.${status}Title`)}
          </Text>
          <Text className="text-sm font-Jakarta text-general-200 dark:text-neutral-400 text-center mt-2">
            {t(`driver.review.${status}Body`)}
          </Text>
        </View>

        {status === "rejected" && profile.rejection_reason ? (
          <View className="bg-white dark:bg-neutral-900 rounded-2xl p-4 mt-4">
            <Text className="text-xs font-JakartaSemiBold uppercase text-general-200 dark:text-neutral-500 mb-1">
              {t("driver.review.reasonLabel")}
            </Text>
            <Text className="font-Jakarta text-black dark:text-white">
              {profile.rejection_reason}
            </Text>
          </View>
        ) : null}

        {status === "pending" ? (
          <TouchableOpacity
            onPress={() => void onRefresh()}
            className="rounded-full py-3 mt-4 items-center border border-neutral-300 dark:border-neutral-800"
          >
            <Text className="font-JakartaBold text-black dark:text-white">
              {t("driver.review.checkAgain")}
            </Text>
          </TouchableOpacity>
        ) : null}

        {status === "rejected" ? (
          <View className="mt-6">
            <Text className="text-lg font-JakartaSemiBold mb-1 text-black dark:text-white">
              {t("driver.review.resubmitTitle")}
            </Text>
            <Text className="text-sm text-general-200 dark:text-neutral-400 font-Jakarta mb-4">
              {t("driver.review.resubmitIntro")}
            </Text>

            <CredentialCapture
              values={credentials}
              onChange={setCredentials}
              documents={documents}
              onDocuments={setDocuments}
              onFile={{
                license: profile.license_image_url !== null,
                id: profile.id_image_url !== null,
                vehicle_reg: profile.vehicle_reg_image_url !== null,
              }}
              // Everything seeded from the rejected profile. national_id is
              // absent because it is deliberately not sent back to the client,
              // so the field starts empty and a scan may fill it freely.
              prefilled={PREFILLED_ON_RESUBMIT}
            />

            <CustomButton
              title={
                submitting ? t("common.saving") : t("driver.review.resubmit")
              }
              onPress={resubmit}
              disabled={submitting}
              className="mt-2"
            />
          </View>
        ) : null}
      </ScrollView>
    </SafeAreaView>
  );
};

// --- Request card ---------------------------------------------------------

const RequestCard = ({
  request,
  busy,
  clockOffset,
  onOffer,
  onWithdraw,
}: {
  request: OpenRequest;
  busy: boolean;
  clockOffset: number;
  onOffer: () => void;
  onWithdraw: () => void;
}) => {
  const t = useT();

  // Redraw once a second so the countdown actually counts. The dashboard poll
  // is every 4s, which is too coarse for a clock the driver is reading.
  const [, setTick] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => setTick((n) => n + 1), 1000);
    return () => clearInterval(timer);
  }, []);

  // How long the job stays on the board. Under the broadcast model this is
  // the request's own life, not a per-driver deadline: nothing passes to
  // anybody when it runs out, the request simply dies unpicked.
  const elapsed =
    (Date.now() + clockOffset - Date.parse(request.created_at)) / 1000;
  const remaining = Math.max(0, Math.ceil(REQUEST_TTL_SECONDS - elapsed));
  const fraction = Math.max(0, Math.min(1, remaining / REQUEST_TTL_SECONDS));
  const urgent = remaining <= 20;

  const offered = request.my_offer_id !== null;
  const km = Math.round(request.pickup_distance_m / 100) / 10;

  // Somebody else wanting the job is information the driver is entitled to:
  // it's the difference between "I'll think about it" and "offer now".
  const rivals = Math.max(0, request.offer_count - (offered ? 1 : 0));

  return (
    <View
      className={`bg-white dark:bg-neutral-900 rounded-2xl p-4 mb-3 ${
        offered ? "border border-emerald-500" : ""
      }`}
    >
      <View className="flex-row items-center justify-between mb-2">
        <Text className="text-sm font-JakartaBold text-primary-500">
          {t("driver.offerCard.newRequest", { service: request.service })}
        </Text>
        <Text className="text-xs text-general-200 dark:text-neutral-400">
          {rivals > 0
            ? t("driver.offerCard.rivals", undefined, rivals)
            : t("driver.offerCard.firstIn")}
        </Text>
      </View>

      {/* How long the job is on the board for. */}
      <View className="mb-3">
        <View className="flex-row items-center justify-between mb-1">
          <Text className="text-xs text-general-200 dark:text-neutral-400">
            {t("driver.offerCard.openFor")}
          </Text>
          <Text
            className={`text-xs font-JakartaBold ${
              urgent ? "text-rose-500" : "text-black dark:text-white"
            }`}
          >
            {t("driver.offerCard.seconds", { n: String(remaining) })}
          </Text>
        </View>
        <View className="h-1.5 rounded-full bg-neutral-200 dark:bg-neutral-800 overflow-hidden">
          <View
            className={`h-full rounded-full ${
              urgent ? "bg-rose-500" : "bg-emerald-500"
            }`}
            style={{ width: `${fraction * 100}%` }}
          />
        </View>
      </View>

      {/* Distance to the pickup: the single most useful thing to know before
          putting your name on a job. Computed server-side from the same
          position dispatch matched on, so it agrees with what got you here. */}
      <View className="flex-row items-center justify-between mb-2">
        <View className="flex-row items-center gap-x-2">
          <MaterialCommunityIcons
            name="map-marker-distance"
            size={16}
            color="#0286ff"
          />
          <Text className="text-xs font-JakartaBold text-primary-500">
            {t("driver.offerCard.awayFromPickup", { km })}
          </Text>
        </View>
        {request.rider_name ? (
          <View className="flex-row items-center gap-x-1">
            <Text className="text-xs text-general-200 dark:text-neutral-400">
              {request.rider_name}
            </Text>
            {request.rider_rating ? (
              <Text className="text-xs text-general-200 dark:text-neutral-400">
                ★ {Number(request.rider_rating).toFixed(1)}
              </Text>
            ) : null}
          </View>
        ) : null}
      </View>

      <View className="flex-row items-center gap-x-2 mb-1">
        <Image
          source={icons.to}
          alt={t("driver.offerCard.fromAlt")}
          className="w-4 h-4"
        />
        <Text
          className="font-JakartaMedium text-black dark:text-white"
          numberOfLines={1}
        >
          {request.origin_address}
        </Text>
      </View>
      <View className="flex-row items-center gap-x-2 mb-3">
        <Image
          source={icons.point}
          alt={t("driver.offerCard.toAlt")}
          className="w-4 h-4"
        />
        <Text
          className="font-JakartaMedium text-black dark:text-white"
          numberOfLines={1}
        >
          {request.destination_address}
        </Text>
      </View>

      <View className="flex-row justify-between mb-3">
        <Text className="text-general-200 dark:text-neutral-400 text-xs">
          {t("driver.offerCard.tripTime")}
        </Text>
        <Text className="font-JakartaMedium text-xs text-black dark:text-white">
          {formatTime(request.ride_time)}
        </Text>
      </View>
      <View className="flex-row justify-between mb-3">
        <Text className="text-general-200 dark:text-neutral-400 text-xs">
          {t("driver.offerCard.youEarn")}
        </Text>
        <Text className="font-JakartaBold text-sm text-emerald-600 dark:text-emerald-400">
          ${(request.payout_cents / 100).toFixed(2)}
        </Text>
      </View>

      {/* Offered already: the wait is on the rider, and the only move left is
          to take it back. Said plainly, because an offer that looks like a
          booking is how a driver ends up parked outside a pickup that was
          never theirs. */}
      {offered ? (
        <>
          <View className="flex-row items-center justify-center gap-x-2 mb-2">
            <ActivityIndicator size="small" color="#10b981" />
            <Text className="text-xs font-JakartaBold text-emerald-600 dark:text-emerald-400">
              {t("driver.offerCard.waitingOnRider")}
            </Text>
          </View>
          <TouchableOpacity
            onPress={onWithdraw}
            disabled={busy}
            className="rounded-full py-3 items-center border border-neutral-300 dark:border-neutral-700"
          >
            <Text className="font-JakartaBold text-neutral-700 dark:text-neutral-200">
              {t("driver.offerCard.withdraw")}
            </Text>
          </TouchableOpacity>
        </>
      ) : (
        <TouchableOpacity
          onPress={onOffer}
          disabled={busy}
          className="rounded-full py-3 bg-emerald-500 items-center"
        >
          <Text className="font-JakartaBold text-white">
            {busy ? "…" : t("driver.offerCard.offer")}
          </Text>
        </TouchableOpacity>
      )}
    </View>
  );
};

// --- Active ride card -----------------------------------------------------

const ActiveRideCard = ({
  ride,
  busy,
  onArrived,
  onStartTrip,
  onComplete,
  onCancel,
  driverCoords,
}: {
  ride: ActiveRide;
  busy: boolean;
  onArrived: (rideId: number) => void;
  onStartTrip: () => void;
  onComplete: (ride: ActiveRide) => void;
  onCancel: () => void;
  driverCoords: { latitude: number; longitude: number } | null;
}) => {
  const t = useT();
  const statusLabel =
    ride.status === "accepted"
      ? t("driver.activeRide.headToPickup")
      : ride.status === "arrived"
        ? t("driver.activeRide.atPickup")
        : ride.status === "en_route"
          ? t("driver.activeRide.tripInProgress")
          : ride.status;

  // Before the rider is aboard the driver is heading to the pickup; after,
  // to the drop-off. Both the map and the navigation handoff follow this.
  const heading = ride.status === "en_route" ? "dropoff" : "pickup";

  const openNavigation = () => {
    const lat =
      heading === "pickup" ? ride.origin_latitude : ride.destination_latitude;
    const lng =
      heading === "pickup" ? ride.origin_longitude : ride.destination_longitude;

    // The universal Maps URL opens the native Google Maps app when it's
    // installed and falls back to the browser when it isn't, on both
    // platforms — no per-platform scheme juggling and no extra dependency.
    const url = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}&travelmode=driving`;

    Linking.openURL(url).catch((err) => {
      console.log("[DRIVER_NAVIGATE]: ", err);
      Alert.alert(
        t("driver.activeRide.alertErrorTitle"),
        t("driver.activeRide.alertNavigateBody"),
      );
    });
  };

  return (
    <View className="bg-primary-500/10 border border-primary-500 rounded-2xl p-4 mb-4">
      <View className="flex-row items-center justify-between mb-2">
        <Text className="text-sm font-JakartaBold text-primary-500">
          ● {statusLabel}
        </Text>
        <Text className="text-xs text-general-200 dark:text-neutral-400">
          {ride.service}
        </Text>
      </View>

      {ride.rider_name ? (
        <View className="flex-row items-center justify-between mb-2">
          <View className="flex-row items-center">
            <Text className="font-JakartaBold text-black dark:text-white">
              {t("driver.activeRide.rider", { name: ride.rider_name })}
            </Text>
            {/* Riders are rated too — a driver should know who they're
                picking up before they pull over for them. */}
            {ride.rider_rating ? (
              <Text className="ml-2 text-xs text-general-200 dark:text-neutral-400">
                ★ {Number(ride.rider_rating).toFixed(1)}
              </Text>
            ) : null}
          </View>
          <View className="flex-row gap-2">
            <TouchableOpacity
              onPress={() => router.push("/(root)/driver-chat")}
              accessibilityLabel={t("driver.activeRide.message")}
              className="w-9 h-9 rounded-full bg-primary-500 items-center justify-center"
            >
              <MaterialCommunityIcons
                name="message-text"
                size={18}
                color="white"
              />
            </TouchableOpacity>
            <TouchableOpacity
              onPress={() =>
                router.push({
                  pathname: "/(root)/call",
                  params: {
                    rideId: String(ride.ride_id),
                    role: "driver",
                    mode: "start",
                  },
                })
              }
              accessibilityLabel={t("driver.activeRide.call")}
              className="w-9 h-9 rounded-full bg-emerald-500 items-center justify-center"
            >
              <MaterialCommunityIcons name="phone" size={18} color="white" />
            </TouchableOpacity>
          </View>
        </View>
      ) : null}

      {/* The map stays up for the whole ride. It used to disappear the moment
          the trip started — exactly when the driver needs the route most —
          leaving them with a destination address as plain text. Before pickup
          it points at the rider; once they're aboard, at the drop-off. */}
      <View className="h-40 rounded-xl overflow-hidden mb-3">
        <Map
          originOverride={driverCoords}
          destinationOverride={
            heading === "pickup"
              ? {
                  latitude: ride.origin_latitude,
                  longitude: ride.origin_longitude,
                  label: t("driver.activeRide.pickupPin"),
                }
              : {
                  latitude: ride.destination_latitude,
                  longitude: ride.destination_longitude,
                  label: t("driver.activeRide.dropoffPin"),
                }
          }
        />
      </View>

      {/* Hand off to whatever navigation app the driver actually uses. The
          in-app map shows the shape of the trip; it is not turn-by-turn. */}
      <TouchableOpacity
        onPress={openNavigation}
        className="flex-row items-center justify-center gap-x-2 rounded-full py-3 mb-3 bg-primary-500"
      >
        <MaterialCommunityIcons
          name="navigation-variant"
          size={18}
          color="white"
        />
        <Text className="font-JakartaBold text-white">
          {heading === "pickup"
            ? t("driver.activeRide.navigateToPickup")
            : t("driver.activeRide.navigateToDropoff")}
        </Text>
      </TouchableOpacity>

      <View className="flex-row items-center gap-x-2 mb-1">
        <Image
          source={icons.to}
          alt={t("driver.activeRide.fromAlt")}
          className="w-4 h-4"
        />
        <Text
          className="font-JakartaMedium text-black dark:text-white"
          numberOfLines={1}
        >
          {ride.origin_address}
        </Text>
      </View>
      <View className="flex-row items-center gap-x-2 mb-3">
        <Image
          source={icons.point}
          alt={t("driver.activeRide.toAlt")}
          className="w-4 h-4"
        />
        <Text
          className="font-JakartaMedium text-black dark:text-white"
          numberOfLines={1}
        >
          {ride.destination_address}
        </Text>
      </View>

      <View className="flex-row justify-between mb-4">
        <Text className="text-general-200 dark:text-neutral-400 text-xs">
          {t("driver.activeRide.youEarn")}
        </Text>
        <Text className="font-JakartaBold text-sm text-emerald-600 dark:text-emerald-400">
          ${(ride.payout_cents / 100).toFixed(2)}
        </Text>
      </View>

      {/* Heading to the pickup: the only forward action is "I'm here". */}
      {ride.status === "accepted" ? (
        <>
          <CustomButton
            title={busy ? "…" : t("driver.activeRide.imHere")}
            bgVariant="success"
            onPress={() => onArrived(ride.ride_id)}
            className="mb-2"
          />
          <TouchableOpacity
            onPress={onCancel}
            disabled={busy}
            className="rounded-full py-3 items-center border border-rose-300 dark:border-rose-900"
          >
            <Text className="font-JakartaBold text-rose-500">
              {t("driver.activeRide.cancelRide")}
            </Text>
          </TouchableOpacity>
        </>
      ) : null}

      {/* At the pickup, waiting on the rider and their code. */}
      {ride.status === "arrived" ? (
        <>
          <Text className="text-xs text-general-200 dark:text-neutral-400 mb-2 text-center">
            {t("driver.activeRide.askForCode")}
          </Text>
          <CustomButton
            title={busy ? "…" : t("driver.activeRide.startTrip")}
            bgVariant="success"
            onPress={onStartTrip}
            className="mb-2"
          />
          <TouchableOpacity
            onPress={onCancel}
            disabled={busy}
            className="rounded-full py-3 items-center border border-rose-300 dark:border-rose-900"
          >
            <Text className="font-JakartaBold text-rose-500">
              {t("driver.activeRide.cancelRide")}
            </Text>
          </TouchableOpacity>
        </>
      ) : null}

      {ride.status === "en_route" ? (
        <CustomButton
          title={busy ? "…" : t("driver.activeRide.completeTrip")}
          bgVariant="success"
          onPress={() => onComplete(ride)}
        />
      ) : null}
    </View>
  );
};

export default DriverHome;
