import React, { useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
  Backdrop,
  Box,
  Button,
  Card,
  Chip,
  IconButton,
  Stack,
  Tab,
  Tabs,
  Tooltip,
  Typography
} from '@mui/material';
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { HiOutlineArrowLeft } from 'react-icons/hi';
import { IoReload } from 'react-icons/io5';
import { MdTwoWheeler } from 'react-icons/md';

import {
  createAutomationDeliveries,
  createOptimisationDeliveries,
  finalCreatedeliveries,
  notifyRider,
  reconcileSteps
} from '../api/api';
import { OpenToast } from 'components/nearle_components/OpenToast';
import CSVExport from 'components/third-party/ReactTable';
import CircularLoader from 'components/nearle_components/CircularLoader';
import Dispatch from './Dispatch';
import { stepColor } from './dispatchShared';

const tuningTypes = [
  { tuneid: 1, type: 'Balanced', value: 'balanced' },
  { tuneid: 2, type: 'Aggressive Speed', value: 'aggressive_speed' },
  { tuneid: 3, type: 'Fuel Saver', value: 'fuel_saver' },
  { tuneid: 4, type: 'Zone Strict', value: 'zone_strict' }
];

// Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for
// the Reconcile tab UI and the reconcile-API payload.
const extractRiders = (previewData) => {
  if (!previewData) return [];
  const map = new Map();
  // De-dupe by orderid across the whole tree. A rider can legitimately appear
  // in multiple zones (one per delivery suburb), so the same rider_id is
  // visited more than once. Without this guard, any stale copy left behind
  // by applyReconcileResponse gets concatenated into the rider's orders and
  // the same orderid is sent twice to /deliveries/createdeliveries.
  const seenOrderIds = new Set();
  const push = (riderId, riderName, orders) => {
    if (riderId == null) return;
    const key = String(riderId);
    if (!map.has(key)) {
      map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
    }
    const entry = map.get(key);
    (orders || []).forEach((o) => {
      const oid = o?.orderid != null ? String(o.orderid) : null;
      if (oid) {
        if (seenOrderIds.has(oid)) return;
        seenOrderIds.add(oid);
      }
      entry.orders.push(o);
    });
    if (!entry.rider_name && riderName) entry.rider_name = riderName;
  };

  if (Array.isArray(previewData.zones) && previewData.zones.length) {
    previewData.zones.forEach((z) => {
      (z.riders || []).forEach((r) => {
        const id = r.rider_id ?? r.userid;
        const name = r.rider_name || r.username || `Rider ${id}`;
        push(id, name, r.orders);
      });
    });
  } else if (Array.isArray(previewData.details)) {
    previewData.details.forEach((o) => {
      const id = o.rider_id ?? o.userid;
      const name = o.rider_name || o.ridername || `Rider ${id}`;
      push(id, name, [o]);
    });
  }
  return Array.from(map.values());
};

// Reverse of extractRiders — flatten rider-grouped list into a details-style
// array (used as the Assign Orders payload).
const flattenRiders = (riders) => {
  const out = [];
  riders.forEach((r) => {
    // Go backend types Deliveries.userid as int — coerce here so any
    // upstream string (AI response, riders API, change-rider edit) gets
    // normalised before the JSON body is built.
    const ridNum = Number(r.rider_id);
    const rid = Number.isFinite(ridNum) ? ridNum : r.rider_id;
    (r.orders || []).forEach((o) => {
      out.push({
        ...o,
        rider_id: rid,
        userid: rid,
        rider_name: r.rider_name,
        rider: r.rider_name
      });
    });
  });
  return out;
};

// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
// zone containment), then rebuilds the flat details list from the new tree.
const applyReconcileResponse = (preview, response) => {
  if (!preview || !Array.isArray(response?.riders)) return preview;
  const next = JSON.parse(JSON.stringify(preview));

  const newOrdersByRider = new Map(
    response.riders.map((r) => [String(r.rider_id), r.orders || []])
  );

  if (Array.isArray(next.zones) && next.zones.length) {
    // Pass 1: wipe every existing copy of a responding rider's orders across
    // ALL zones. The server's reconciled list is the single source of truth,
    // and a rider can be present in multiple zones (one per delivery suburb).
    // The previous "update first match, delete from map" loop left stale
    // copies in the other zones, which extractRiders then concatenated into
    // duplicate orderids — surfacing as duplicate deliveries on Assign.
    next.zones.forEach((zone) => {
      if (!Array.isArray(zone.riders)) return;
      zone.riders.forEach((r) => {
        const key = String(r.rider_id ?? r.userid);
        if (newOrdersByRider.has(key)) r.orders = [];
      });
    });

    // Pass 2: drop the reconciled orders onto the first zone that already
    // lists the rider. If the rider isn't anywhere in the tree, append a
    // fresh rider entry to zone[0].
    newOrdersByRider.forEach((orders, riderKey) => {
      let placed = false;
      for (const zone of next.zones) {
        if (!Array.isArray(zone.riders)) continue;
        const target = zone.riders.find(
          (r) => String(r.rider_id ?? r.userid) === riderKey
        );
        if (target) {
          target.orders = orders;
          placed = true;
          break;
        }
      }
      if (!placed) {
        const target = next.zones[0];
        target.riders = target.riders || [];
        target.riders.push({
          rider_id: Number(riderKey) || riderKey,
          rider_name: orders[0]?.rider_name || `Rider ${riderKey}`,
          orders
        });
      }
    });
  } else {
    next.zones = [
      {
        zone_name: 'Reconciled',
        riders: response.riders.map((r) => ({
          rider_id: r.rider_id,
          rider_name: r.rider_name || `Rider ${r.rider_id}`,
          orders: r.orders || []
        }))
      }
    ];
  }

  // Rebuild flat details from the updated zones->riders->orders tree.
  const flatDetails = [];
  next.zones.forEach((zone) => {
    (zone.riders || []).forEach((r) => {
      (r.orders || []).forEach((o) => {
        flatDetails.push({
          ...o,
          rider_id: r.rider_id,
          userid: r.rider_id,
          rider_name: r.rider_name,
          rider: r.rider_name
        });
      });
    });
  });
  next.details = flatDetails;

  return next;
};

const Preview = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const stateData = location.state || {};

  // SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes
  // through this state. The Dispatch tab renders from it, the Reconcile tab
  // derives its rider list from it, and Assign Orders sends a flattened copy
  // of it to the API.
  const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);

  // The AI response arrives via location.state, which the browser stores in
  // history.state and persists across reloads. That means a reload of
  // /dispatch/preview would re-hydrate the stale snapshot — including any
  // pending edits the user thought they discarded. Bounce to /orders when
  // there's no fresh response, and wipe the history snapshot once consumed
  // so a later reload / back-forward also bounces instead of re-using it.
  useEffect(() => {
    if (!stateData.dispatchPreviewData) {
      navigate('/nearle/orders', { replace: true });
      return;
    }
    if (typeof window !== 'undefined' && window.history?.state) {
      window.history.replaceState({ ...window.history.state, usr: null }, '');
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, []);
  const [csvExportData, setCsvExportData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [tabValue, setTabValue] = useState(0);

  const [reconcileLoading, setReconcileLoading] = useState(false);
  const [hasReconciled, setHasReconciled] = useState(false);

  // Tracks riders whose orders have been edited since the last AI response
  // or successful reconcile. Only these are sent to the reconcile API — the
  // server-side step re-ordering only needs to see what actually changed.
  const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set());

  const aiMode = stateData.aiMode ?? 1;
  const selectedMode = stateData.selectedMode || null;
  const deliveryData = stateData.deliveryData || [];
  const autoRiders = stateData.autoRiders || [];
  const absentRidersPayload = stateData.absentRidersPayload || [];
  const rider = stateData.rider || null;

  const appId = useMemo(() => {
    if (stateData.appId) return stateData.appId;
    if (typeof window !== 'undefined') {
      const v = localStorage.getItem('applocationid');
      return v ? Number(v) : 0;
    }
    return 0;
  }, [stateData.appId]);

  // Derived: rider list for the Reconcile tab. Recomputes whenever the cache
  // (dispatchPreviewData) changes — so Reconcile reflects here without a separate state.
  const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);

  // Derived: flat orders list used for the Assign Orders payload + CSV export.
  // Always reflects the latest cache state.
  const finaldeliveryList = useMemo(() => {
    const flat = flattenRiders(reconcileRiders);
    if (flat.length) return computeDeliveryAmounts(flat);
    if (Array.isArray(dispatchPreviewData?.details)) {
      return computeDeliveryAmounts(dispatchPreviewData.details);
    }
    return [];
  }, [reconcileRiders, dispatchPreviewData]);

  useEffect(() => {
    const filtered = finaldeliveryList.map((item) => ({
      zone_name: item.zone_name,
      ordernotes: item.ordernotes,
      rider: item.rider,
      step: item.step,
      ordertype: item.ordertype,
      orderamount: item.orderamount,
      riderkms: item.riderkms,
      cumulativekms: item.cumulativekms,
      baseprice: item.baseprice,
      minkm: item.minkm,
      priceperkm: item.priceperkm,
      kms: item.kms,
      actualkms: item.actualkms,
      rider_charge: item.rider_charge,
      deliveryamt: item.deliveryamt,
      deliverycharges: item.deliverycharges,
      profit: item.profit
    }));
    setCsvExportData(filtered);
  }, [finaldeliveryList]);

  const notifyRiderMutation = useMutation({
    mutationFn: notifyRider,
    onSuccess: () => OpenToast('Notification sent Successfully', 'success', 2000),
    onError: (error) => OpenToast(error.message, 'error', 2000)
  });

  const createDeliveryMutation = useMutation({
    mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
    onSuccess: (data) => {
      OpenToast('Orders Optimised  Successfully', 'success', 2000);
      // Brand new response = brand new source of truth.
      setDispatchPreviewData(data);
      setHasReconciled(false);
      setDirtyRiderIds(new Set());
      setIsLoading(false);
    },
    onError: (error) => {
      OpenToast(error.message, 'error', 4000);
      setIsLoading(false);
    },
    onSettled: () => setIsLoading(false)
  });

  const createFinalDeliveryMutation = useMutation({
    mutationFn: finalCreatedeliveries,
    onSuccess: () => {
      OpenToast('Delivery Created Successfully', 'success', 2000);
      setIsLoading(false);
      if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
      navigate('/nearle/deliveries');
    },
    onError: (error) => {
      OpenToast(error.message, 'error', 4000);
      setIsLoading(false);
    },
    onSettled: () => setIsLoading(false)
  });

  const reconcileMutation = useMutation({
    mutationFn: reconcileSteps,
    onMutate: () => setReconcileLoading(true),
    onSuccess: (data) => {
      if (Array.isArray(data?.riders)) {
        // Merge: applyReconcileResponse replaces orders for riders present
        // in the response and leaves the rest of the cache untouched.
        setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
        setHasReconciled(true);
        // Clear only the riders we just reconciled from the dirty set, so
        // any unrelated edits made meanwhile are preserved.
        setDirtyRiderIds((prev) => {
          const next = new Set(prev);
          data.riders.forEach((r) => next.delete(String(r.rider_id)));
          return next;
        });
        OpenToast('Steps reconciled — preview updated', 'success', 2000);
      } else {
        OpenToast('Reconcile returned no rider data', 'warning', 3000);
      }
    },
    onError: (error) => {
      OpenToast(error.message || 'Reconcile failed', 'error', 4000);
    },
    onSettled: () => setReconcileLoading(false)
  });

  const handleCreateDelivery = (tune) => {
    setIsLoading(true);
    if (aiMode == 0) {
      createDeliveryMutation.mutate({ deliveries: deliveryData });
    } else if (selectedMode && selectedMode?.value == 1) {
      createDeliveryMutation.mutate({
        deliveries: deliveryData,
        hypertuning_params: tune || null,
        selectedMode,
        absent_riders: absentRidersPayload
      });
    } else {
      createDeliveryMutation.mutate({
        data: {
          orders: deliveryData,
          riders: autoRiders,
          config: { pay_type: 'hourly', base_pay: 300.0, strategy: 'multi_trip' },
          absent_riders: absentRidersPayload
        },
        selectedMode
      });
    }
  };

  const handleFinalCreateDelivery = () => {
    if (!finaldeliveryList?.length) {
      OpenToast('No deliveries to assign', 'error', 3000);
      return;
    }
    setIsLoading(true);
    createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
  };

  const handleReconcile = () => {
    if (!reconcileRiders.length) {
      OpenToast('No riders to reconcile', 'warning', 3000);
      return;
    }
    // Only send riders that were edited since the last AI response / reconcile.
    // Their step ordering is the only thing that can be stale — untouched
    // riders are skipped to keep the payload small.
    const dirty = reconcileRiders.filter((r) =>
      dirtyRiderIds.has(String(r.rider_id))
    );
    if (!dirty.length) {
      OpenToast('No edits to reconcile', 'info', 2500);
      return;
    }
    reconcileMutation.mutate({
      riders: dirty.map((r) => ({
        rider_id: r.rider_id,
        orders: r.orders
      }))
    });
  };

  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: 'column',
        height: { xs: 'calc(100vh - 56px)', sm: 'calc(100vh - 64px)' },
        overflow: 'hidden',
        position: 'relative',
        mt: { xs: -2, sm: -3 },
        mx: { xs: -2, sm: -3 }
      }}
    >
      <Backdrop
        sx={{ position: 'absolute', color: '#fff', zIndex: (theme) => theme.zIndex.modal + 1 }}
        open={isLoading}
      >
        <CircularLoader color="inherit" />
      </Backdrop>

      <Box
        sx={{
          py: 1.5,
          px: 2.5,
          borderBottom: '1px solid #e2e8f0',
          background: 'linear-gradient(135deg, rgba(102,37,130,0.06) 0%, rgba(146,85,171,0.06) 100%)',
          flexShrink: 0
        }}
      >
        <Stack direction="row" alignItems="center" justifyContent="space-between">
          <Stack direction="row" alignItems="center" spacing={1.5}>
            <Tooltip title="Back to orders" placement="top">
              <IconButton
                onClick={() => navigate('/nearle/orders')}
                sx={{
                  bgcolor: '#ffffff',
                  border: '1px solid #e2e8f0',
                  color: '#662582',
                  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
                  '&:hover': { bgcolor: '#f8fafc', borderColor: '#662582' }
                }}
              >
                <HiOutlineArrowLeft size={18} />
              </IconButton>
            </Tooltip>
            <Typography variant="h3" fontWeight={700} sx={{ color: '#0f172a' }}>
              Assign Orders
            </Typography>
          </Stack>

          <Button
            variant="contained"
            onClick={handleFinalCreateDelivery}
            sx={{
              borderRadius: 999,
              px: 3.5,
              py: 0.875,
              bgcolor: '#662582',
              color: '#ffffff',
              textTransform: 'none',
              fontWeight: 800,
              fontSize: 13,
              boxShadow: '0 4px 14px rgba(102, 37, 130, 0.25)',
              '&:hover': {
                bgcolor: '#9255ab',
                transform: 'translateY(-1px)',
                boxShadow: '0 6px 20px rgba(102, 37, 130, 0.35)'
              },
              transition: 'all 0.2s'
            }}
          >
            Assign Orders
          </Button>
        </Stack>
      </Box>

      <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        {dispatchPreviewData && (
          <Dispatch
            key={dispatchPreviewData?.__cacheKey || JSON.stringify(reconcileRiders.length)}
            data={dispatchPreviewData}
            embedded
          />
        )}
      </Box>

    </Box>
  );
};

// Mirrors the orders.js deliveryamt recalc — applied at render-time so the
// Assign payload always reflects the current cache without a useEffect.
function computeDeliveryAmounts(list) {
  return list.map((item) => {
    const cumulativeKms = Number(item.cumulativekms || 0);
    const minKm = Number(item.minkm || 0);
    const basePrice = Number(item.baseprice || 0);
    const pricePerKm = Number(item.priceperkm || 0);
    if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
    return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
  });
}

export default Preview;
