import datetime
import json
import logging
import re
from collections import defaultdict
from decimal import Decimal

from django.conf import settings
from django.contrib import messages
from django.core.cache import cache
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.views.decorators.http import require_POST

from accounts.account_delete import delete_or_suspend_self
from accounts.decorators import role_required
from accounts.models import AccountType
from accounts.supabase_client import get_client

from . import cart as cart_utils
from .ai_assistant import get_ingredients_for_dish, match_ingredients_to_products
from .chat_assistant import (
    DEFAULT_MODE,
    MODES as CHAT_MODES,
    apply_cart_action,
    apply_save_plan_action,
    get_chat_reply,
)
from .llm_client import call_llm
from .chat_memory import (
    clear_chat_history,
    clear_chat_memory,
    load_chat_history,
    load_chat_memory,
    save_chat_history,
    save_chat_memory,
)
from .chat_checkout import describe_available_delivery_days, schedule_order_from_chat
from .checkout_state import clear_checkout_state, save_checkout_state
from .customer_location import get_named_locations, get_saved_location as get_customer_saved_location
from .known_areas import learn_area_from_location
from .emailer import send_contact_message
from .markets import get_market, get_nearest_market, get_picker_origin_coords
from .picker_availability import (
    get_active_order_load_batch,
    get_all_b2b_pickers_for_truck_type,
    get_available_b2b_pickers_for_date,
    get_order_counts_for_date_batch,
    get_picker_status,
    has_capacity_for_date,
)
from .picker_ranking import rank_pickers
from .fleet_ranking import group_and_rank_providers
from .regions import resolve_region_for_location
from .site_settings import get_b2b_max_orders_per_day, get_contact_settings, get_delivery_weekdays, get_market_radius_options_km, get_max_orders_per_day, get_multi_stop_pickup_enabled
from .site_url import absolute_url
from .geo import (
    DAR_ES_SALAAM_CENTER,
    fuzzy_match_area,
    geocode,
    get_multi_stop_route,
    get_route,
    get_route_surface_summary,
    haversine_km,
    is_within_service_area,
    reverse_geocode,
    reverse_geocode_components,
    split_pickup_delivery_distance,
    traffic_adjusted_duration,
)
from .messaging import get_messages, get_unseen_message_count, mark_messages_seen, send_message
from .order_emails import get_b2b_invoice_data, notify_new_message, notify_order_placed, send_b2b_invoice
from .push_notifications import push_new_message, push_order_status_changed, save_subscription
from .order_notifications import mark_all_orders_seen, mark_order_seen
from .order_status import current_step_index, get_status_label, get_tracker_steps
from .payment_methods import build_whatsapp_proof_link, get_active_payment_methods
from .price_trends import get_overall_trend_badge, get_trend_badges_for_products
from .pricing import apply_vendor_fee_waivers, calculate_b2b_delivery_fee, calculate_delivery_fee, calculate_package_fee, calculate_picking_fee, get_effective_price, get_min_qty
from .product_i18n import get_product_badges, get_product_display_names, localize_product
from .measure_units import get_unit_label_map, per_each_unit_display, per_unit_display, quantity_unit_display
from .product_measures import get_active_measures_for_product, get_active_measures_for_products, get_measure_min_qty
from .truck_types import get_active_truck_types, get_truck_type
from .ratings import get_picker_rating_summary_batch, get_platform_rating_summary, get_rating_for_order, submit_rating
from .saved_plans import delete_all_plans, delete_plan, get_active_plans, get_plan, mark_plan_purchased
from .scheduling import TIME_SLOTS, next_delivery_dates, next_delivery_dates_for_company, quick_delivery_available, remaining_slots_today
from .sw_dates import format_delivery_days_note, format_month_day, format_weekday
from .spending_analytics import get_spending_dashboard
from .supabase_orders import create_order_and_items
from .translations import LANGUAGES, get_t

logger = logging.getLogger(__name__)


def _account_type(request):
    picker_user = request.session.get('picker_user') or {}
    return picker_user.get('account_type')


def _visible(query):
    """Wraps a pickker_products query with both visibility filters a
    customer-facing read site must apply: not soft-deleted (is_active) and
    not still awaiting admin approval (is_approved — see
    fleet_create_product below). One helper instead of two separate
    .eq() calls at every call site specifically because two independent
    passes over this exact codebase each missed a real site doing that by
    hand — this removes the "remembered one filter, forgot the other" risk."""
    return query.eq('is_active', True).eq('is_approved', True)


def _trending_commodities(lang='en'):
    resp = (
        _visible(get_client().table('pickker_products').select('*'))
        .eq('is_trending', True)
        .order('name')
        .limit(10)
        .execute()
    )
    commodities = resp.data
    badges = get_trend_badges_for_products([p['id'] for p in commodities])
    unit_label_map = get_unit_label_map(lang)
    for product in commodities:
        product['unit_display'] = unit_label_map.get(product['unit'], product['unit'])
        badge = badges.get(product['id'])
        if badge and badge['change_pct'] is not None:
            # Real, admin-recorded regional price data takes priority over
            # the product's own default catalog price for this ticker.
            product['farm_price'] = badge['farm_price']
            product['market_price'] = badge['market_price']
            product['trend_direction'] = badge['direction']
            product['trend_change_pct'] = badge['change_pct']
        else:
            # No trend is ever hand-typed — if there isn't yet enough
            # recorded regional price history (Admin Portal → Regional
            # Price Trends) to compute a real one, show none at all rather
            # than a stale or fabricated percentage.
            product['trend_direction'] = None
            product['trend_change_pct'] = None
    return commodities


@require_POST
def set_language(request):
    lang = request.POST.get('language')
    if lang in dict(LANGUAGES):
        request.session['language'] = lang
        picker_user = request.session.get('picker_user')
        if picker_user:
            get_client().table('pickker_users').update({'preferred_language': lang}).eq('id', picker_user['id']).execute()
    next_url = request.POST.get('next') or request.META.get('HTTP_REFERER') or '/'
    return redirect(next_url)


def home(request):
    from .site_content import (
        get_active_news, get_active_partners, get_hero_image_url, get_home_text, get_how_to_content,
        get_partners_heading, to_embeddable_video_url,
    )

    how_to = get_how_to_content()
    return render(request, 'market/home.html', {
        'commodities': _trending_commodities(request.session.get('language', 'en')),
        'hero_image_url': get_hero_image_url(),
        'home_text': get_home_text(),
        'how_to': how_to,
        'how_to_video_embed_url': to_embeddable_video_url(how_to['how_to_video_url']) if how_to['how_to_video_url'] else None,
        'news_items': get_active_news(),
        'partners': get_active_partners(),
        'partners_heading': get_partners_heading(),
    })


def plan_order(request):
    return render(request, 'market/plan_order.html')


def market(request):
    from concurrent.futures import ThreadPoolExecutor

    from .categories import get_all_categories, get_category_display_map, localize_categories
    from .customer_location import get_saved_location
    from .markets import get_market, get_market_categories
    from .personalization import get_customer_product_quantities

    query = _visible(get_client().table('pickker_products').select('*'))
    category = request.GET.get('category', '').strip()
    if category:
        query = query.eq('category', category)
    search = request.GET.get('q', '').strip()
    if search:
        # Matches the product's name (either language) or its category, so
        # typing a category word (e.g. "vegetables" or "mboga") surfaces
        # every product in it, not just a product literally named that.
        query = query.or_(f'name.ilike.%{search}%,name_sw.ilike.%{search}%,category.ilike.%{search}%')

    lang = request.session.get('language', 'en')

    # Read once, up front (a pure session lookup, no cost to moving it
    # earlier) so a logged-in customer's order-history lookup AND their
    # saved-location lookup (used further down for the region/radius
    # defaults and "near you" sort) can both join the SAME parallel batch
    # below instead of the saved-location one costing its own separate,
    # sequential round trip after this batch finishes.
    picker_user = request.session.get('picker_user')
    account_type = _account_type(request)

    # These five top-level lookups don't depend on each other, so they run
    # in parallel; badges/measures need the product ids from the first one,
    # so they're a second parallel round below.
    with ThreadPoolExecutor(max_workers=5) as executor:
        products_future = executor.submit(lambda: query.order('name').execute().data)
        commodities_future = executor.submit(_trending_commodities, lang)
        categories_future = executor.submit(get_all_categories)
        personalization_future = executor.submit(get_customer_product_quantities, picker_user['id']) if picker_user else None
        location_future = executor.submit(get_saved_location, picker_user['id']) if picker_user and account_type is not None else None
        products = products_future.result()
        commodities = commodities_future.result()
        categories = categories_future.result()
        product_quantities = personalization_future.result() if personalization_future else {}
        customer_location = location_future.result() if location_future else None

    if search:
        from .admin_analytics import log_search
        log_search(search, len(products))

    # Which registered markets actually have a product tied to them right
    # now — the "filter by market" dropdown only ever offers real, live
    # options, same reasoning as the category filter. Computed from this
    # same unfiltered product list, before the market_id filter (if any)
    # narrows it below, so filtering by one market never removes the others
    # from the dropdown itself.
    tied_market_ids = {p['market_id'] for p in products if p.get('market_id')}
    tied_markets = sorted((get_market(mid) for mid in tied_market_ids), key=lambda m: m['name'])

    selected_market_id = request.GET.get('market_id', '').strip()
    selected_market = None
    if selected_market_id:
        products = [p for p in products if str(p.get('market_id')) == selected_market_id]
        selected_market = get_market(int(selected_market_id)) if selected_market_id.isdigit() else None

    # Warehouse/Industry filter — a separate dropdown from the market one
    # above (never both narrow the list at once; picking one clears the
    # other via the form), same "only offer real, live options" sourcing.
    from .vendors import get_vendor
    tied_vendor_keys = {(p['vendor_type'], p['vendor_id']) for p in products if p.get('vendor_type') in ('warehouse', 'industry') and p.get('vendor_id')}
    tied_vendors = sorted(
        (v for v in (({**(get_vendor(vt, vid) or {}), 'vendor_type': vt, 'vendor_id': vid, 'vendor_key': f'{vt}:{vid}'}) for vt, vid in tied_vendor_keys) if v.get('name')),
        key=lambda v: v['name'],
    )
    selected_vendor_key = request.GET.get('vendor', '').strip()
    selected_vendor = None
    if selected_vendor_key and ':' in selected_vendor_key:
        sel_type, _, sel_id = selected_vendor_key.partition(':')
        products = [p for p in products if p.get('vendor_type') == sel_type and str(p.get('vendor_id')) == sel_id]
        selected_vendor = next((v for v in tied_vendors if v['vendor_type'] == sel_type and str(v['vendor_id']) == sel_id), None)

    # Region filter — a third independent dropdown, same "only offer real,
    # live options" sourcing as market/vendor above. Every product's vendor
    # (market/warehouse/industry) is resolved once here and reused below
    # both for this filter and for the "near you" distance computation, so
    # a product with no vendor tie at all simply has no region (never
    # hidden by this filter, just absent from it — same fail-open shape as
    # the B2C region gate at checkout).
    from .regions import get_regions_by_id, localize_region, resolve_region_for_location

    def _product_vendor(p):
        if p.get('vendor_type') and p.get('vendor_id'):
            return get_vendor(p['vendor_type'], p['vendor_id'])
        if p.get('market_id'):
            return get_market(p['market_id'])
        return None

    for p in products:
        vendor = _product_vendor(p)
        p['_vendor'] = vendor
        p['region_id'] = vendor.get('region_id') if vendor else None

    tied_region_ids = {p['region_id'] for p in products if p.get('region_id')}
    regions_by_id = get_regions_by_id(tied_region_ids)
    tied_regions = sorted(regions_by_id.values(), key=lambda r: r['name_en'])

    # Resolved here (moved up from where the "near you" distance block used
    # to compute it) so both the region default just below AND the radius
    # default further down can use it. "explicit" distinguishes a fresh
    # page load (apply the customer's own region + admin's default radius
    # automatically) from the customer having actually touched the filter
    # form themselves — including hitting "Clear filters" (?filtered=1)
    # with everything else blank, which must mean "show me everything," not
    # "reapply the defaults I never asked to leave."
    explicit = request.GET.get('filtered') == '1'
    # account_type/customer_location were already resolved above (the
    # latter fetched in the same parallel batch as products/categories/etc.)
    customer_region_id = (
        resolve_region_for_location(customer_location['lat'], customer_location['lng'])
        if customer_location else None
    )

    # B2B is inherently interregional (a wholesale buyer isn't limited to
    # "nearby" markets the way a B2C shopper is) -- the region/radius
    # defaults below are a B2C-only convenience. A B2B customer can still
    # explicitly turn either one on via the same filter controls.
    is_b2c_default_candidate = account_type != 'b2b'

    selected_region_id = request.GET.get('region_id', '').strip()
    if not selected_region_id and not explicit and customer_region_id and is_b2c_default_candidate:
        selected_region_id = str(customer_region_id)
    if selected_region_id:
        # A product with no vendor tie at all has no region_id (None) —
        # str(None) would never equal a real region id, so without this
        # explicit carve-out every generic/global product would silently
        # vanish the moment ANY region filter is active. Latent in the
        # original opt-in filter (rarely exercised); now load-bearing since
        # the region filter applies by default on every page load.
        products = [p for p in products if not p.get('region_id') or str(p.get('region_id')) == selected_region_id]

    localize_categories(categories, lang)
    category_display_map = get_category_display_map(lang)
    unit_label_map = get_unit_label_map(lang)
    selected_market_categories = [category_display_map.get(c, c) for c in get_market_categories(selected_market)] if selected_market else []
    product_ids = [p['id'] for p in products]
    with ThreadPoolExecutor(max_workers=2) as executor:
        badges_future = executor.submit(get_trend_badges_for_products, product_ids)
        measures_future = executor.submit(get_active_measures_for_products, product_ids)
        badges = badges_future.result()
        measures_by_product = measures_future.result()
    for product in products:
        localize_product(product, lang)
        product['category_display'] = category_display_map.get(product['category'], product['category'])
        product['effective_price'] = get_effective_price(product, account_type)
        product['min_qty'] = get_min_qty(product, account_type)
        product['price_badge'] = badges.get(product['id'])
        product['unit_display'] = unit_label_map.get(product['unit'], product['unit'])
        product['per_unit_display'] = per_unit_display(product['unit'], lang)
        product['min_qty_unit_display'] = quantity_unit_display(product['min_qty'], product['unit'], lang)
        product['region_display'] = localize_region(regions_by_id.get(product.get('region_id')), lang)
        measures = measures_by_product.get(product['id'], [])
        for m in measures:
            m['label'] = unit_label_map.get(m['measure_type'], m['measure_type'])
        product['active_measures'] = measures
        if account_type == 'b2b' and product.get('b2b_price') is not None:
            retail = Decimal(str(product['market_price']))
            wholesale = Decimal(str(product['b2b_price']))
            if retail > 0:
                product['bulk_savings_pct'] = round(float((retail - wholesale) / retail * 100))

    # Personalization — a logged-in customer with real order history sees
    # categories they actually buy surface first, replacing (not fighting)
    # the plain alphabetical default below. Rolled up from product_id-level
    # quantities to CATEGORY here, using this page's own already-decorated
    # product list (pickker_order_items has no category column, and a
    # product_id from past history may no longer even be in today's active
    # catalog) — never a second database query. Scored by category, not
    # exact product, so two same-category variants (e.g. "Rice" from two
    # markets) stay adjacent for comparison, same reasoning as the grouping
    # sort right below. Empty when there's no history at all (new customer,
    # or logged out) — fails open, sort is untouched in that case.
    category_scores = defaultdict(int)
    if product_quantities:
        product_by_id = {p['id']: p for p in products}
        for product_id, qty in product_quantities.items():
            matched = product_by_id.get(product_id)
            if matched:
                category_scores[matched['category']] += qty

    # Same-named products (e.g. "Rice" tied to two different markets, each
    # with its own grade/price) sort next to each other so a customer
    # comparing variants sees them side by side instead of scattered across
    # the grid — grouped on the same label they actually see (display_name),
    # not the raw English `name` column the query itself ordered by. The
    # personalization score (when there is one) is prepended ahead of that
    # grouping, not a replacement for it — the later "near you" distance
    # re-sort below still overrides both when it applies, same as it
    # already overrides plain alphabetical ordering today.
    if category_scores:
        products.sort(key=lambda p: (-category_scores.get(p['category'], 0), p['display_name'].lower(), p.get('market_name') or ''))
    else:
        products.sort(key=lambda p: (p['display_name'].lower(), p.get('market_name') or ''))

    # "Near you" — only meaningful when we actually know where the customer
    # is (their saved delivery location, resolved above), and only for
    # products tied to a market/warehouse/industry (that's the only thing
    # with a real lat/lng to measure from). A product with no vendor tie
    # has no distance at all — it's never hidden by the radius filter, just
    # excluded from the near/far sort (sorts after everything with a known
    # distance). Same explicit/default split as the region filter above: a
    # fresh page load applies the admin's default radius (+ sorts nearest-
    # first) automatically; an explicit filter submission uses exactly what
    # the customer set, including "nothing" (all distances) if they cleared it.
    sort_near = request.GET.get('sort') == 'near'
    radius_km_raw = request.GET.get('radius_km', '').strip()
    radius_km = float(radius_km_raw) if radius_km_raw.replace('.', '', 1).isdigit() else None
    if radius_km is None and not explicit and customer_location and is_b2c_default_candidate:
        from .site_settings import get_default_market_radius_km
        radius_km = get_default_market_radius_km()
        sort_near = True

    if customer_location:
        for product in products:
            vendor = product.get('_vendor')
            if vendor and vendor.get('lat') is not None and vendor.get('lng') is not None:
                product['distance_km'] = round(
                    haversine_km(customer_location['lat'], customer_location['lng'], float(vendor['lat']), float(vendor['lng'])), 2,
                )
            else:
                product['distance_km'] = None

        if radius_km is not None:
            products = [p for p in products if p['distance_km'] is None or p['distance_km'] <= radius_km]

        known_distances = [p['distance_km'] for p in products if p['distance_km'] is not None]
        if known_distances:
            nearest_km = min(known_distances)
            for p in products:
                if p['distance_km'] is not None:
                    p['distance_tag'] = 'closest' if p['distance_km'] <= nearest_km * 1.3 else 'farther'

        if sort_near:
            products.sort(key=lambda p: p['distance_km'] if p['distance_km'] is not None else float('inf'))

    return render(request, 'market/market.html', {
        'products': products,
        'is_b2b': account_type == 'b2b',
        'commodities': commodities,
        'categories': categories,
        'selected_category': category,
        'search_query': search,
        'tied_markets': tied_markets,
        'selected_market_id': selected_market_id,
        'selected_market': selected_market,
        'selected_market_categories': selected_market_categories,
        'tied_vendors': tied_vendors,
        'selected_vendor_key': selected_vendor_key,
        'selected_vendor': selected_vendor,
        'tied_regions': [{'id': r['id'], 'display': localize_region(r, lang)} for r in tied_regions],
        'selected_region_id': selected_region_id,
        'customer_region_id': customer_region_id,
        'has_customer_location': customer_location is not None,
        'sort_near': sort_near,
        'radius_km': radius_km,
        'radius_options': get_market_radius_options_km(),
    })


def market_search_suggestions(request):
    """Backs the market page's search-as-you-type dropdown — a handful of
    matching product names and category names (each tagged so the frontend
    can label them), same name/name_sw/category match as the real search so
    picking a suggestion always leads to a non-empty result page."""
    query = request.GET.get('q', '').strip()
    if len(query) < 2:
        return JsonResponse({'results': []})

    client = get_client()
    lang = request.session.get('language', 'en')

    products_resp = (
        _visible(client.table('pickker_products').select('name, name_sw, category'))
        .or_(f'name.ilike.%{query}%,name_sw.ilike.%{query}%')
        .limit(6)
        .execute()
    )
    from .categories import get_category_display_map

    category_display_map = get_category_display_map(lang)
    seen = set()
    results = []
    for p in products_resp.data:
        label = (p.get('name_sw') if lang == 'sw' and p.get('name_sw') else p['name'])
        if label.lower() in seen:
            continue
        seen.add(label.lower())
        results.append({'label': label, 'type': 'product'})

    categories_resp = client.table('pickker_categories').select('name, name_sw').or_(f'name.ilike.%{query}%,name_sw.ilike.%{query}%').limit(4).execute()
    for c in categories_resp.data:
        label = category_display_map.get(c['name'], c['name'])
        if label.lower() in seen:
            continue
        seen.add(label.lower())
        results.append({'label': label, 'type': 'category'})

    return JsonResponse({'results': results[:8]})


@role_required('customer')
@require_POST
def toggle_business_mode(request):
    from .fleet import get_company_for_user

    customer_id = request.session['picker_user']['id']
    current = _account_type(request)
    new_type = AccountType.B2C if current == AccountType.B2B else AccountType.B2B
    t = get_t(request)

    if new_type == AccountType.B2C and get_company_for_user(customer_id):
        # Can't drop back to a plain personal account while still owning a
        # registered fleet company — that company would be left with an
        # owner who's no longer even a business account.
        messages.error(request, t['msg_cannot_switch_owns_fleet'])
        return redirect(request.POST.get('next') or 'market:market')

    get_client().table('pickker_customer_profiles').update({'account_type': new_type}).eq('user_id', customer_id).execute()
    request.session['picker_user']['account_type'] = new_type
    request.session.modified = True
    if new_type == AccountType.B2B:
        messages.success(request, t['msg_business_mode_on'])
    else:
        messages.success(request, t['msg_business_mode_off'])
    return redirect(request.POST.get('next') or 'market:market')


def about(request):
    return render(request, 'market/about.html')


def legal_policy(request, policy_type):
    from market.legal_policies import get_policy, localize_policy

    policy = get_policy(policy_type)
    if not policy:
        raise Http404
    policy = localize_policy(policy, request.session.get('language', 'en'))
    return render(request, 'market/legal_policy.html', {'policy': policy})


def contact(request):
    contact_settings = get_contact_settings()
    if request.method == 'POST':
        t = get_t(request)
        name = request.POST.get('name', '').strip()
        email = request.POST.get('email', '').strip()
        message = request.POST.get('message', '').strip()
        if not name or not email or not message:
            messages.error(request, t['msg_contact_fill_required'])
        else:
            try:
                get_client().table('pickker_contact_messages').insert({'name': name, 'email': email, 'message': message}).execute()
            except Exception:
                pass  # never let admin-analytics logging block the customer's actual message
            if send_contact_message(name, email, message, contact_settings['support_email']):
                messages.success(request, t['msg_contact_sent'])
                return redirect('market:contact')
            else:
                messages.error(request, t['msg_contact_failed'])
    return render(request, 'market/contact.html', {
        'contact_support_email': contact_settings['support_email'],
        'contact_inquiry_email': contact_settings['inquiry_email'],
        'contact_direct_email': contact_settings['direct_email'],
        'contact_phone_numbers': contact_settings['phone_number_list'],
        'contact_office_address': contact_settings['office_address'],
    })


def _cart_items_and_subtotal(customer_id, account_type=None, lang='en', cart=None):
    """Builds the priced/weighed cart line items. A row with a `measure_id`
    uses that measure's own admin-set price and kg_equivalent; a row with no
    measure (the common case for products nobody's configured alternative
    containers for) falls back to the product's plain unit price, and its
    weight is approximated as 1kg per unit — see the picking-fee note in
    market/pricing.py for why.

    `cart` lets a caller that already fetched the raw cart rows (e.g.
    checkout_location, which needs them upfront anyway to bounce an empty
    cart) pass them straight in instead of triggering a second identical
    Supabase round trip — every checkout step is a remote HTTPS call, so
    each avoided round trip is real, measurable latency off that click."""
    if cart is None:
        cart = cart_utils.get_cart(customer_id)
    items = []
    subtotal = Decimal('0')
    if cart:
        unit_label_map = get_unit_label_map(lang)
        product_ids = [row['product_id'] for row in cart]
        resp = _visible(get_client().table('pickker_products').select('*')).in_('id', product_ids).execute()
        products_by_id = {p['id']: localize_product(p, lang) for p in resp.data}
        for p in products_by_id.values():
            p['unit_display'] = unit_label_map.get(p['unit'], p['unit'])

        measure_ids = [row['measure_id'] for row in cart if row['measure_id']]
        measures_by_id = {}
        if measure_ids:
            m_resp = get_client().table('pickker_product_measures').select('*').in_('id', measure_ids).execute()
            measures_by_id = {m['id']: m for m in m_resp.data}

        for row in cart:
            product = products_by_id.get(row['product_id'])
            if not product:
                continue
            qty = row['quantity']
            measure = measures_by_id.get(row['measure_id']) if row['measure_id'] else None

            if measure:
                unit_price = Decimal(str(measure['price']))
                kg_equivalent = Decimal(str(measure['kg_equivalent']))
                measure_key = measure['measure_type']
                measure_label = unit_label_map.get(measure_key, measure_key)
                min_qty = get_measure_min_qty(measure)
            else:
                unit_price = get_effective_price(product, account_type)
                kg_equivalent = Decimal('1')
                measure_key = product['unit']
                measure_label = None
                min_qty = get_min_qty(product, account_type)

            line_total = unit_price * qty
            subtotal += line_total
            items.append({
                'cart_item_id': row['id'],
                'product': product,
                'quantity': qty,
                'line_total': line_total,
                'unit_price': unit_price,
                'min_qty': min_qty,
                'measure': measure,
                'measure_label': measure_label,
                'qty_unit_display': quantity_unit_display(qty, measure_key, lang),
                'weight_kg': kg_equivalent * qty,
            })
    return items, subtotal


def _total_weight_kg(items):
    return sum((item['weight_kg'] for item in items), Decimal('0'))


def _total_quantity(items):
    return sum((item['quantity'] for item in items), 0)


def _distinct_items_by_category(items):
    """{category: count of distinct products in it} for this cart/order —
    feeds the picking fee's category-diversity surcharge (see
    market/pricing.py::calculate_picking_fee). Distinct *products*, not
    quantity — buying 10kg of one vegetable is one stop, same as 1kg."""
    product_ids_by_category = {}
    for item in items:
        category = item['product'].get('category') or ''
        product_ids_by_category.setdefault(category, set()).add(item['product']['id'])
    return {category: len(ids) for category, ids in product_ids_by_category.items()}


def _required_market_ids(items):
    """Distinct market_id values among cart items tied to one specific
    market (a product only sourced from one shop/area — see
    market/markets.py) — empty for a fully generic cart (nothing changes
    for that, the common case), one id if every tied item agrees on the
    same market, or more than one if the cart mixes items unique to
    different markets, which no single picker can fulfill."""
    return {item['product']['market_id'] for item in items if item['product'].get('market_id')}


def _has_generic_items(items):
    """True if at least one cart item is NOT tied to a specific market —
    used to decide whether a mixed order (exactly one tied market plus
    generic items) should get a nearest-registered-market suggestion for
    the generic portion (see get_nearest_market/_checkout_picker_b2c)."""
    return any(not item['product'].get('market_id') for item in items)


def _required_vendor_ties(items):
    """Distinct (vendor_type, vendor_id) among cart items tied to a
    Warehouse or Industry (see market/vendors.py) — deliberately excludes
    'market' ties, which keep going through the existing, untouched
    _required_market_ids/market_id path above. A fully generic cart, or one
    only mixing generic + market-tied items, returns an empty set (the
    common case — nothing about it changes)."""
    return {
        (item['product']['vendor_type'], item['product']['vendor_id'])
        for item in items
        if item['product'].get('vendor_type') in ('warehouse', 'industry') and item['product'].get('vendor_id')
    }


@role_required('customer')
def add_to_cart(request, product_id):
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
    if request.method == 'POST':
        customer_id = request.session['picker_user']['id']
        account_type = _account_type(request)
        product_resp = _visible(get_client().table('pickker_products').select('*')).eq('id', product_id).execute()
        product = product_resp.data[0] if product_resp.data else None
        t = get_t(request)
        if not product:
            # Not just missing — could be soft-deleted or still awaiting
            # admin approval (see fleet_create_product). Either way it must
            # never actually reach the cart; the qty/min-qty fallbacks below
            # only cover the *display* case, they don't block the add on
            # their own.
            if is_ajax:
                return JsonResponse({'ok': False, 'message': t['msg_product_not_available']}, status=404)
            messages.error(request, t['msg_product_not_available'])
            return redirect(request.POST.get('next') or 'market:market')

        lang = request.session.get('language', 'en')

        measure_id = request.POST.get('measure_id') or None
        if measure_id:
            measure_id = int(measure_id)

        # A measure line (bucket/sack/etc.) has its own admin-set minimum,
        # independent of the product's plain-unit minimum.
        if measure_id:
            measure_resp = get_client().table('pickker_product_measures').select('*').eq('id', measure_id).execute()
            measure = measure_resp.data[0] if measure_resp.data else None
            min_qty = get_measure_min_qty(measure)
            unit_key = measure['measure_type'] if measure else 'unit'
        else:
            min_qty = get_min_qty(product, account_type) if product else 1
            unit_key = product['unit'] if product else 'unit'

        qty = int(request.POST.get('quantity') or min_qty)
        message_text = t['msg_added_to_cart']
        message_tag = 'success'
        if qty < min_qty:
            qty = min_qty
            message_text = t['msg_min_qty_added'].format(qty_unit=quantity_unit_display(min_qty, unit_key, lang), min_qty=min_qty)
            message_tag = 'info'

        cart_utils.add_to_cart(customer_id, product_id, qty, measure_id=measure_id)

        if is_ajax:
            cart_count = sum(row['quantity'] for row in cart_utils.get_cart(customer_id))
            return JsonResponse({'ok': True, 'cart_count': cart_count, 'message': message_text, 'tag': message_tag})

        messages.add_message(request, messages.SUCCESS if message_tag == 'success' else messages.INFO, message_text)
    elif is_ajax:
        return JsonResponse({'ok': False}, status=405)
    return redirect(request.POST.get('next') or 'market:market')


@role_required('customer')
def cart_view(request):
    customer_id = request.session['picker_user']['id']
    account_type = _account_type(request)
    items, subtotal = _cart_items_and_subtotal(customer_id, account_type, lang=request.session.get('language', 'en'))

    # One-shot: popped whether or not a market was actually found, so a
    # hard block with no nearby market on file doesn't leave a stale key
    # behind for a later, unrelated cart visit to pick up.
    suggestion = request.session.pop(_HARD_BLOCK_MARKET_SESSION_KEY, None)
    request.session.modified = True
    suggested_market = None
    if suggestion:
        suggested_market = get_market(suggestion['id'])
        if suggested_market:
            suggested_market = {**suggested_market, 'distance_km': suggestion['distance_km']}

    return render(request, 'market/cart.html', {
        'items': items, 'subtotal': subtotal, 'is_b2b': account_type == 'b2b',
        'suggested_market': suggested_market,
    })


@role_required('customer')
def update_cart_item(request, cart_item_id):
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
    message_text = None
    message_tag = 'info'
    if request.method == 'POST':
        customer_id = request.session['picker_user']['id']
        account_type = _account_type(request)
        action = request.POST.get('action')

        cart_row = next((row for row in cart_utils.get_cart(customer_id) if row['id'] == cart_item_id), None)
        if cart_row:
            qty = cart_row['quantity']

            # A measure line (bucket/sack/etc.) has its own admin-set minimum,
            # independent of the product's plain-unit minimum.
            if cart_row['measure_id']:
                measure_resp = get_client().table('pickker_product_measures').select('*').eq('id', cart_row['measure_id']).execute()
                measure = measure_resp.data[0] if measure_resp.data else None
                min_qty = get_measure_min_qty(measure)
            else:
                product_resp = _visible(get_client().table('pickker_products').select('*')).eq('id', cart_row['product_id']).execute()
                product = product_resp.data[0] if product_resp.data else None
                min_qty = get_min_qty(product, account_type) if product else 1

            if action == 'increase':
                cart_utils.set_cart_item_qty(cart_item_id, qty + 1)
            elif action == 'decrease':
                new_qty = qty - 1
                if 0 < new_qty < min_qty:
                    message_text = get_t(request)['msg_min_qty_remove'].format(min_qty=min_qty)
                else:
                    cart_utils.set_cart_item_qty(cart_item_id, new_qty)
            elif action == 'remove':
                cart_utils.remove_cart_item(cart_item_id)

        if is_ajax:
            items, subtotal = _cart_items_and_subtotal(customer_id, account_type, lang=request.session.get('language', 'en'))
            html = render_to_string('market/_cart_body.html', {
                'items': items, 'subtotal': subtotal, 't': get_t(request),
            }, request=request)
            # cart_count from the items just priced above, not a second
            # full get_cart() round trip — this endpoint is the +/- qty
            # button, clicked repeatedly while a customer adjusts their
            # cart, so every avoided round trip here is felt directly.
            cart_count = sum(item['quantity'] for item in items)
            return JsonResponse({'html': html, 'cart_count': cart_count, 'message': message_text, 'tag': message_tag})

        if message_text:
            messages.info(request, message_text)
    elif is_ajax:
        return JsonResponse({'ok': False}, status=405)
    return redirect('market:cart')


def _vendor_order_eligibility(vendor, checkout, total_weight_kg):
    """Classifies a B2C cart against the ONE warehouse/industry vendor it's
    tied to (see _required_vendor_ties) — the close/far/out-of-range
    distance zones and per-zone minimum-order enforcement described in the
    Phase 1 plan. Called twice per checkout (see _needs_provider_step for
    the early routing check, checkout_schedule/_checkout_picker_b2c for the
    authoritative re-check immediately before the order is actually
    created — the cart can change in between, same "re-checked at the
    moment of booking" reasoning has_capacity_for_date already uses).

    Returns (zone, effective_min_kg, eligible):
    - zone: 'close' | 'far' | 'out_of_range'
    - effective_min_kg: the real minimum basket weight this order must
      meet for its zone (None when out_of_range — there's no minimum that
      would help, the vendor simply can't deliver this far at all)
    - eligible: True only when in range AND total_weight_kg meets it
    """
    from .site_settings import get_close_radius_km, get_far_radius_km
    from .vendors import get_effective_min_order_kg

    if not vendor or vendor.get('lat') is None or vendor.get('lng') is None:
        return 'out_of_range', None, False

    distance_km = haversine_km(checkout['lat'], checkout['lng'], float(vendor['lat']), float(vendor['lng']))
    close_radius_km = get_close_radius_km()
    far_radius_km = get_far_radius_km()

    if distance_km <= close_radius_km:
        zone = 'close'
    elif distance_km <= far_radius_km:
        zone = 'far'
    else:
        return 'out_of_range', None, False

    effective_min_kg = get_effective_min_order_kg(vendor, zone)
    eligible = total_weight_kg >= effective_min_kg
    return zone, effective_min_kg, eligible


def _needs_provider_step(request, customer_id, items=None):
    """True when this checkout must go through the new checkout_provider
    step (company/individual fleet-picker selection) before scheduling —
    a B2B account, or any cart with a Warehouse/Industry vendor tie
    (_checkout_picker_vendor_fleet's case), regardless of account type.
    False (plain B2C) keeps today's location -> schedule -> picker order
    completely unchanged.

    One carve-out (Phase 1 close-radius routing): a B2C cart tied to
    exactly one warehouse/industry, close enough to it and meeting that
    zone's minimum, skips the truck/company flow entirely and returns
    False here — falling through to the ordinary B2C picker flow instead,
    since a truck should never be dispatched for a small nearby order (see
    _vendor_order_eligibility). A multi-vendor cart, a far-zone/below-
    minimum cart, or one whose vendor has gone missing/lost its location
    all keep returning True unchanged — checkout_provider's own existing
    guards (mixed-vendor error, msg_truck_unavailable) still apply to
    those exactly as before this carve-out existed.

    `items` lets a caller that already priced the cart this request (e.g.
    checkout_location, which also has to call _vendor_hard_block) pass it
    straight in instead of re-fetching and re-pricing the same cart a
    second time over the network."""
    if _account_type(request) == 'b2b':
        return True
    if items is None:
        items, _ = _cart_items_and_subtotal(customer_id, _account_type(request))
    vendor_ties = _required_vendor_ties(items)
    if not vendor_ties:
        return False
    if len(vendor_ties) > 1:
        return True

    checkout = request.session.get('checkout') or {}
    if checkout.get('lat') is None or checkout.get('lng') is None:
        # No location known yet (shouldn't happen at any real call site —
        # all five guarantee lat/lng first — but fail toward the existing,
        # already-safe truck/company path rather than guessing).
        return True

    vendor_type, vendor_id = next(iter(vendor_ties))
    from .vendors import get_vendor
    vendor = get_vendor(vendor_type, vendor_id)
    total_weight_kg = _total_weight_kg(items)
    zone, _effective_min_kg, eligible = _vendor_order_eligibility(vendor, checkout, total_weight_kg)
    return not (zone == 'close' and eligible)


_HARD_BLOCK_MARKET_SESSION_KEY = 'hard_block_market_suggestion_id'


def _flash_market_suggestion(request, checkout):
    """One-shot hint for cart_view: the nearest real market to browse
    instead, shown once right after a hard block. Deliberately a separate,
    distinctly-named session key from checkout['suggested_market_id'] (an
    unrelated feature — a picker-routing waypoint for mixed orders, see
    _route_for_candidate/shop_payments.py::record_suggested_market) so the
    two can never be confused or cross-wired. Stores id AND distance_km
    together — get_market() (used later in cart_view to resolve the id)
    only returns the raw market row, which has no distance_km field at
    all; that value only ever exists as get_nearest_market()'s own computed
    output, so it has to be carried across the redirect here or it's lost."""
    from .markets import get_nearest_market
    nearest = get_nearest_market(checkout.get('lat'), checkout.get('lng'))
    request.session[_HARD_BLOCK_MARKET_SESSION_KEY] = {'id': nearest['id'], 'distance_km': nearest['distance_km']} if nearest else None
    request.session.modified = True


def _vendor_hard_block(request, customer_id, checkout, items=None):
    """True (and sets a translated flash message) when a single-vendor B2C
    cart cannot be fulfilled by its tied warehouse/industry at all — either
    it's entirely out of the vendor's far_radius_km, or it's within
    far_radius_km but below that zone's minimum. Per this phase's economic
    rule, that vendor is never offered in this case (no truck should ever
    be sent for a tiny far-away order, and there's no picker fallback once
    outside the close radius) — unlike close-zone-below-minimum, which
    _needs_provider_step already routes to the ordinary truck/company flow
    instead of blocking. B2B carts are never blocked here (trucks/companies
    have no minimum-order rule); neither are multi-vendor carts
    (checkout_provider's own existing mixed-vendor guard already covers
    that case)."""
    if _account_type(request) == 'b2b':
        return False
    if items is None:
        items, _ = _cart_items_and_subtotal(customer_id, _account_type(request))
    vendor_ties = _required_vendor_ties(items)
    if len(vendor_ties) != 1:
        return False
    if checkout.get('lat') is None or checkout.get('lng') is None:
        return False

    vendor_type, vendor_id = next(iter(vendor_ties))
    from .vendors import get_vendor
    vendor = get_vendor(vendor_type, vendor_id)
    total_weight_kg = _total_weight_kg(items)
    zone, effective_min_kg, eligible = _vendor_order_eligibility(vendor, checkout, total_weight_kg)
    if zone == 'close' or eligible:
        return False

    t = get_t(request)
    if zone == 'out_of_range':
        messages.error(request, t['msg_vendor_out_of_range'])
    else:
        messages.error(request, t['msg_vendor_below_far_minimum'].format(min_kg=effective_min_kg))
    _flash_market_suggestion(request, checkout)
    return True


@role_required('customer')
def checkout_location(request):
    customer_id = request.session['picker_user']['id']
    cart = cart_utils.get_cart(customer_id)
    if not cart:
        messages.error(request, get_t(request)['msg_cart_empty'])
        return redirect('market:market')

    if request.method == 'POST':
        t = get_t(request)
        lat = request.POST.get('lat')
        lng = request.POST.get('lng')
        address = request.POST.get('address', '').strip()
        confirmed = request.POST.get('location_confirmed') == '1'
        if not lat or not lng:
            messages.error(request, t['msg_select_location'])
        elif not confirmed:
            messages.error(request, t['msg_confirm_location'])
        else:
            checkout = {
                'address': address,
                'lat': float(lat),
                'lng': float(lng),
            }
            request.session['checkout'] = checkout
            request.session.modified = True
            # One combined write instead of two separate updates to the same
            # pickker_customer_profiles row (checkout_state, then address/
            # lat/lng right after) — each Supabase round trip from here is a
            # real ~150-300ms over the network, so merging these saves one
            # full round trip on every single checkout-location submission.
            # Changing the location at checkout also updates the customer's
            # registered delivery location, so next time it auto-loads here.
            get_client().table('pickker_customer_profiles').update({
                'checkout_state': checkout,
                'address': address,
                'location_lat': float(lat),
                'location_lng': float(lng),
            }).eq('user_id', customer_id).execute()
            # If this pin is nowhere near an area already on file, learn it —
            # the gazetteer keeps growing from real customer usage, not just
            # admin input or AI-resolved searches.
            learn_area_from_location(address, float(lat), float(lng))
            # Priced once here and threaded into both checks below instead of
            # each one re-fetching and re-pricing the same cart itself — was
            # 3 redundant cart/product/measure round trips per submission.
            items, _ = _cart_items_and_subtotal(customer_id, _account_type(request), cart=cart)
            if _vendor_hard_block(request, customer_id, checkout, items=items):
                return redirect('market:cart')
            if _needs_provider_step(request, customer_id, items=items):
                return redirect('market:checkout_provider')
            return redirect('market:checkout_schedule')

    saved_location = get_customer_saved_location(customer_id)
    named_locations = get_named_locations(customer_id)
    return render(request, 'market/checkout_location.html', {
        'center': DAR_ES_SALAAM_CENTER,
        'saved_location': saved_location,
        'home_location': named_locations['home'],
        'business_location': named_locations['business'],
        'other_location': named_locations['other'],
    })


def _company_has_capacity_for_date(company_id, date_str, b2b_max_orders):
    """Aggregate version of has_capacity_for_date — true if AT LEAST ONE of
    the company's drivers has room on this date. Used to keep
    checkout_schedule from ever offering a date the company could not
    possibly fulfill with any of its own drivers (the "zombie order" risk
    flagged when this flow was planned)."""
    from .fleet import get_company_driver_ids

    driver_ids = get_company_driver_ids(company_id)
    if not driver_ids:
        return False
    counts = get_order_counts_for_date_batch(driver_ids, date_str)
    return any(counts.get(did, 0) < b2b_max_orders for did in driver_ids)


_WEATHER_DELAY_KEYWORDS = ('rain', 'storm', 'thunder', 'shower', 'drizzle', 'flood', 'hail')


def _quick_delivery_weather_note(request, lat, lng):
    """Only meaningful for quick/same-day delivery -- here_weather() only
    ever returns CURRENT conditions, not a forecast, so it can't inform a
    future Mon/Wed/Sat scheduled date. Shares the exact cache key/TTL
    weather_data (this module) already uses for the map's own weather
    badge, so this never triggers a second, redundant HERE call for the
    same rounded location. Fails open: any missing key, API error, or
    non-delay-prone conditions just means no note -- never blocks
    checkout."""
    from .here_api import here_weather

    lat, lng = round(float(lat), 2), round(float(lng), 2)
    cache_key = f'weather_{lat}_{lng}'
    weather = cache.get(cache_key, 'MISS')
    if weather == 'MISS':
        weather = here_weather(lat, lng)
        cache.set(cache_key, weather, 600)
    if not weather:
        return None

    text = f"{weather.get('description') or ''} {weather.get('sky_desc') or ''}".lower()
    if any(keyword in text for keyword in _WEATHER_DELAY_KEYWORDS):
        return get_t(request)['schedule_weather_delay_note']
    return None


@role_required('customer')
def checkout_schedule(request):
    checkout = request.session.get('checkout')
    if not checkout or 'lat' not in checkout:
        return redirect('market:checkout_location')

    customer_id = request.session['picker_user']['id']
    lang = request.session.get('language', 'en')
    t = get_t(request)
    items, subtotal = _cart_items_and_subtotal(customer_id, _account_type(request), lang=lang)
    total_weight_kg = _total_weight_kg(items)

    # B2B/vendor-fleet carts must have already picked a provider (company or
    # individual) at checkout_provider before reaching here — that's now
    # where the "who delivers" decision happens, and this step just needs
    # to know THAT provider's own calendar. A B2B session that somehow lands
    # here without one yet (stale bookmark, back-button) bounces back.
    provider_type = checkout.get('provider_type')
    if _needs_provider_step(request, customer_id) and not provider_type:
        return redirect('market:checkout_provider')

    # Cost-band preview: show the vendor's close/far minimum tiers before
    # the customer can hit _vendor_hard_block's wall below — never for B2B
    # (that hard-block/minimum economics explicitly never applies there,
    # see _vendor_hard_block's own docstring). If provider_type is already
    # set, checkout['vendor_type']/['vendor_id'] are authoritative
    # (checkout_provider guarantees a single non-market vendor whenever
    # both are set); otherwise this is the close-zone carve-out path, where
    # _required_vendor_ties(items) is guaranteed len==1 by construction
    # (that's the only way to reach checkout_schedule with no provider_type
    # and a vendor tie at all — see _needs_provider_step).
    vendor_eligibility = None
    if _account_type(request) != 'b2b':
        from .vendors import get_vendor, get_effective_min_order_kg
        from .site_settings import get_close_radius_km, get_far_radius_km
        vt = vid = None
        if provider_type:
            vt, vid = checkout.get('vendor_type'), checkout.get('vendor_id')
        else:
            vendor_ties = _required_vendor_ties(items)
            if len(vendor_ties) == 1:
                vt, vid = next(iter(vendor_ties))
        if vt and vid:
            vendor = get_vendor(vt, vid)
            if vendor:
                zone, _effective_min_kg, eligible = _vendor_order_eligibility(vendor, checkout, total_weight_kg)
                vendor_eligibility = {
                    'zone': zone,
                    'eligible': eligible,
                    'total_weight_kg': total_weight_kg,
                    'close_note': t['schedule_zone_close_note'].format(radius_km=get_close_radius_km(), min_kg=get_effective_min_order_kg(vendor, 'close')),
                    'far_note': t['schedule_zone_far_note'].format(radius_km=get_far_radius_km(), min_kg=get_effective_min_order_kg(vendor, 'far')),
                }

    quick_available = quick_delivery_available()
    weather_delay_note = _quick_delivery_weather_note(request, checkout['lat'], checkout['lng']) if quick_available else None

    # Far B2B truck order -> a real, calculated multi-day delivery estimate
    # instead of always offering "starting tomorrow". Scoped to genuine B2B
    # truck bookings only (account_type=='b2b'), not a B2C vendor-tied far-
    # zone order routed to a truck via _needs_provider_step -- that case
    # already has its own distinct cost-band UI/economics (vendor_eligibility
    # above) and isn't priced/estimated the same way.
    b2b_estimate = None
    start_offset_days = 1
    if _account_type(request) == 'b2b' and provider_type and checkout.get('distance_km') is not None:
        from .b2b_delivery_estimate import estimate_b2b_delivery_window
        from .site_settings import get_b2b_far_distance_km
        if checkout['distance_km'] >= get_b2b_far_distance_km():
            min_days, max_days = estimate_b2b_delivery_window(
                checkout['distance_km'], total_weight_kg, checkout.get('route_picker_ok'),
            )
            b2b_estimate = {'min_days': min_days, 'max_days': max_days}
            start_offset_days = min_days

    b2b_max_orders = get_b2b_max_orders_per_day()
    if provider_type == 'company':
        candidate_dates = next_delivery_dates_for_company(checkout['company_id'], count=3, start_offset_days=start_offset_days)
        candidate_dates = [d for d in candidate_dates if _company_has_capacity_for_date(checkout['company_id'], d.isoformat(), b2b_max_orders)]
    elif provider_type == 'individual':
        candidate_dates = [d for d in next_delivery_dates(3, start_offset_days=start_offset_days) if has_capacity_for_date(checkout['picker_id'], d.isoformat(), b2b_max_orders)]
    else:
        candidate_dates = next_delivery_dates(3)

    if provider_type and not candidate_dates:
        # Nobody real can actually take this within the offered window —
        # never create an order nobody can fulfill. Send them back to pick
        # a different provider instead.
        messages.error(request, get_t(request)['msg_provider_no_openings'])
        return redirect('market:checkout_provider')

    dates = [
        {'value': d.isoformat(), 'weekday_display': format_weekday(d, lang), 'month_day_display': format_month_day(d, lang)}
        for d in candidate_dates
    ]
    days_note = format_delivery_days_note(get_delivery_weekdays(), lang)
    b2b_estimate_note = (
        t['schedule_b2b_far_estimate_note'].format(min_days=b2b_estimate['min_days'], max_days=b2b_estimate['max_days'])
        if b2b_estimate else None
    )

    if request.method == 'POST':
        is_quick = request.POST.get('quick_delivery') == '1'
        if is_quick and not quick_delivery_available():
            # Re-checked here, not just hidden client-side — a request that
            # started before the cutoff could still be submitted after it.
            messages.error(request, get_t(request)['msg_quick_delivery_ended'])
            return redirect('market:checkout_schedule')
        if is_quick:
            # Not tied to the Mon/Wed/Sat schedule — assigned to a picker for
            # today instead, at a surcharge on the delivery fee (Fee Settings).
            date_str = datetime.date.today().isoformat()
            quick_time = request.POST.get('quick_time') or 'now'
            time_slot = 'Quick Delivery (ASAP)' if quick_time == 'now' else f'{quick_time} (Quick Delivery)'
        else:
            date_str = request.POST.get('delivery_date')
            time_slot = request.POST.get('time_slot')
        if not date_str or not time_slot:
            messages.error(request, get_t(request)['msg_choose_date_time'])
        elif provider_type == 'individual' and not is_quick and not has_capacity_for_date(checkout['picker_id'], date_str, b2b_max_orders):
            # Re-checked at the moment of booking, same "list may be
            # slightly stale" reasoning has_capacity_for_date's own
            # docstring already describes.
            messages.error(request, get_t(request)['msg_truck_just_booked'])
        else:
            checkout.update({'delivery_date': date_str, 'delivery_time_slot': time_slot, 'is_quick': is_quick})
            request.session['checkout'] = checkout
            request.session.modified = True
            save_checkout_state(customer_id, checkout)
            if provider_type:
                # Authoritative last-mile re-check — the cart could have
                # shrunk below the vendor's minimum, or the vendor could
                # have gone out of range, between checkout_location's early
                # check and this final submit (same "list may be slightly
                # stale" reasoning has_capacity_for_date's docstring already
                # describes for dates/pickers).
                if _vendor_hard_block(request, customer_id, checkout, items=items):
                    return redirect('market:cart')
                # Provider already chosen at checkout_provider — this is now
                # the true final step for B2B/vendor-fleet, not checkout_picker.
                # The delivery fee depends on is_quick, which is only known
                # right here (not yet at checkout_provider time), so it's
                # computed fresh now from the distance/truck already locked
                # in at provider selection — never recomputing the route
                # itself, just the quick-delivery surcharge on top of it.
                total_quantity = _total_quantity(items)
                picking_fee = calculate_picking_fee(total_weight_kg, _distinct_items_by_category(items))
                package_fee = calculate_package_fee(total_weight_kg, total_quantity)
                truck_type = get_truck_type(checkout.get('truck_type_id'))
                delivery_fee = calculate_b2b_delivery_fee(checkout['distance_km'], truck_type, is_quick=is_quick, total_weight_kg=total_weight_kg)
                after_create = None
                if checkout.get('vendor_type') and checkout.get('vendor_id'):
                    from .vendors import get_vendor
                    vendor_for_waiver = get_vendor(checkout['vendor_type'], checkout['vendor_id'])
                    delivery_fee, picking_fee = apply_vendor_fee_waivers(vendor_for_waiver, total_weight_kg, delivery_fee, picking_fee)
                    after_create = _make_vendor_fleet_after_create(request, items, checkout['vendor_type'], checkout['vendor_id'])
                return _create_order_and_finish(
                    request, checkout, customer_id, items, subtotal,
                    delivery_fee, picking_fee, truck_type_id=checkout.get('truck_type_id'),
                    package_fee=package_fee, after_create=after_create,
                )
            return redirect('market:checkout_picker')

    return render(request, 'market/checkout_schedule.html', {
        'items': items,
        'subtotal': subtotal,
        'checkout': checkout,
        'dates': dates,
        'days_note': days_note,
        'time_slots': TIME_SLOTS,
        'quick_time_slots': remaining_slots_today(),
        'quick_delivery_available': quick_available,
        'weather_delay_note': weather_delay_note,
        'provider_type': provider_type,
        'vendor_eligibility': vendor_eligibility,
        'b2b_estimate_note': b2b_estimate_note,
    })


def _create_order_and_finish(request, checkout, customer_id, items, subtotal, delivery_fee, picking_fee, truck_type_id=None, package_fee=0, after_create=None):
    """Shared final step for both B2C and B2B checkout — creates the order +
    items, notifies everyone, clears the cart, and redirects to the order.
    after_create, when given, is called with the freshly-created order
    right after it's inserted (e.g. to record a non-market vendor tie) —
    optional and unused by every existing caller, so this changes nothing
    about their behavior. If after_create returns a truthy value, the order
    is treated as cancelled by after_create itself (e.g. an out-of-stock
    line found only at this final atomic check) — no placement notification/
    invoice/instruction message goes out for a dead-on-arrival order, and
    the cart is left untouched so the customer can just retry."""
    # A double-click, a slow-network retry, or a resubmitted form can fire
    # this same "confirm" request twice — the JS submit-guard on the
    # checkout page covers the common case, this is the server-side
    # backstop. If this exact customer already has a matching recent order,
    # treat this as the duplicate and just send them to that order instead
    # of billing them (and decrementing stock) twice. A company-routed order
    # (picker_id still None, company_id set) can't be matched by picker_id,
    # so it's matched by company_id instead — .eq('picker_id', None) would
    # silently match nothing in PostgREST, not "is null", so this must
    # branch rather than share one query shape.
    recent_cutoff = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30)).isoformat()
    total = subtotal + delivery_fee + picking_fee + package_fee
    dup_query = (
        get_client().table('pickker_orders').select('id')
        .eq('customer_id', customer_id).eq('total_amount', str(total)).gte('created_at', recent_cutoff)
    )
    if checkout.get('picker_id'):
        dup_query = dup_query.eq('picker_id', checkout['picker_id'])
    elif checkout.get('company_id'):
        dup_query = dup_query.is_('picker_id', 'null').eq('company_id', checkout['company_id'])
    else:
        dup_query = None
    if dup_query is not None:
        duplicate_resp = dup_query.order('id', desc=True).limit(1).execute()
        if duplicate_resp.data:
            return redirect('market:order_detail', order_id=duplicate_resp.data[0]['id'])

    order, order_items_rows = create_order_and_items(
        customer_id=customer_id,
        picker_id=checkout.get('picker_id'),
        company_id=checkout.get('company_id'),
        address=checkout.get('address', ''),
        lat=checkout['lat'],
        lng=checkout['lng'],
        picker_lat=checkout.get('picker_lat'),
        picker_lng=checkout.get('picker_lng'),
        distance_km=checkout.get('distance_km'),
        pickup_distance_km=checkout.get('pickup_distance_km'),
        delivery_distance_km=checkout.get('delivery_distance_km'),
        items=items,
        subtotal=subtotal,
        delivery_fee=delivery_fee,
        picking_fee=picking_fee,
        package_fee=package_fee,
        delivery_date=checkout['delivery_date'],
        delivery_time_slot=checkout['delivery_time_slot'],
        is_quick=checkout.get('is_quick', False),
        truck_type_id=truck_type_id,
        route_surface_label=checkout.get('route_surface_label'),
        route_paved_pct=checkout.get('route_paved_pct'),
        eta_traffic_min=checkout.get('eta_traffic_min'),
        account_type=_account_type(request),
    )

    if checkout.get('suggested_market_id'):
        from .shop_payments import record_suggested_market
        record_suggested_market(order['id'], checkout['suggested_market_id'])

    if after_create and after_create(order):
        return redirect('market:cart')

    # The customer just created this order themselves, so its starting
    # status shouldn't count as an "unseen" notification.
    mark_order_seen(customer_id, order['id'], order['status'], request=request)
    notify_order_placed(order)
    if _account_type(request) == 'b2b':
        send_b2b_invoice(order, order_items_rows, get_truck_type(truck_type_id))

    # A custom instruction typed at picker-selection time goes in as the
    # order's first message — same thread the picker/customer keep chatting
    # in afterward. Deliberately NOT notifying the picker about it here: the
    # order is still pending_payment, so a picker who got pinged now would
    # have no idea what to do with it. It's surfaced to them, bundled with
    # the order details, only once admin confirms payment (see
    # notify_order_status_changed's 'confirmed' branch) — one notification,
    # not a confusing two-step drip before they're even allowed to act.
    picker_instructions = checkout.get('picker_instructions')
    if picker_instructions:
        instruction_message = send_message(order['id'], customer_id, picker_instructions)
        if instruction_message:
            mark_messages_seen(customer_id, order['id'])

    # Cart/checkout-in-progress only get cleared once the order is actually
    # placed — not on logout, cancel, or session expiry.
    cart_utils.clear_cart(customer_id)
    clear_checkout_state(customer_id)
    request.session.pop('checkout', None)
    request.session.modified = True
    return redirect('market:order_detail', order_id=order['id'])


def _make_vendor_fleet_after_create(request, items, vendor_type, vendor_id):
    """Builds the after_create closure _create_order_and_finish calls right
    after inserting a warehouse/industry-tied order — records the vendor
    tie and atomically decrements real stock, exactly as
    _checkout_picker_vendor_fleet did inline before this flow was
    restructured to defer picker/company selection ahead of scheduling.
    Delegates the actual tie+decrement+rollback logic to
    market/stock.py::decrement_vendor_stock_for_order, shared with the chat
    checkout shortcut (market/chat_checkout.py), which has no Django
    request/messages framework to report a failure through."""
    def _after_create(order):
        from .stock import decrement_vendor_stock_for_order
        failed_product = decrement_vendor_stock_for_order(order['id'], items, vendor_type, vendor_id)
        if failed_product:
            messages.error(request, get_t(request)['msg_out_of_stock'].format(name=failed_product.get('display_name') or failed_product['name']))
            return True
        return False
    return _after_create


def _gather_b2b_candidates(checkout, total_weight_kg, vendor_type=None, vendor_id=None):
    """Ranked top-3 provider options (companies represented by their single
    best-available driver, plus any unaffiliated individual owner-operators
    — see market/fleet_ranking.py::group_and_rank_providers) for a B2B or
    vendor-fleet cart, computed BEFORE a delivery date is chosen (the
    company decides internally which driver/date once the order exists —
    see market/fleet.py::assign_driver_to_order). Shared by both the plain
    B2B truck-type path and the warehouse/industry vendor-fleet path.
    Returns (options, error_key) — error_key is set (and options is None)
    only when a named vendor has gone missing/lost its location."""
    from .fleet import get_picker_ids_for_warehouse, get_truck_ids_for_warehouse
    from .vendors import get_vendor

    truck_types = [t for t in get_active_truck_types(segment='b2b') if float(t['max_weight_kg']) >= float(total_weight_kg)]
    platform_avg_rating = get_platform_rating_summary().get('average')

    origin_lat, origin_lng = checkout['lat'], checkout['lng']
    vendor = None
    if vendor_type and vendor_id:
        vendor = get_vendor(vendor_type, vendor_id)
        if not vendor or vendor.get('lat') is None or vendor.get('lng') is None:
            return None, 'msg_truck_unavailable'
        origin_lat, origin_lng = float(vendor['lat']), float(vendor['lng'])

    # A vendor-tied cart has a real pickup point (the warehouse/industry
    # itself) to fall back a driver's position to if they haven't shared a
    # live GPS fix yet — without this, a company's only driver silently
    # vanishing from candidates just because they'd never opened the app
    # meant the company itself could never be offered at all, even though
    # it legitimately owns the vendor. No such fallback exists for a plain
    # B2B cart (no vendor) — falling back to the customer's own delivery
    # address there would be a much worse assumption, so that path is
    # unchanged.
    fallback_lat, fallback_lng = (origin_lat, origin_lng) if vendor is not None else (None, None)

    all_candidates = []
    for truck_type in truck_types:
        for p in get_all_b2b_pickers_for_truck_type(truck_type['id'], fallback_lat=fallback_lat, fallback_lng=fallback_lng):
            all_candidates.append({
                **p,
                'truck_type_id': truck_type['id'],
                'distance_km': haversine_km(origin_lat, origin_lng, p['lat'], p['lng']),
            })

    if vendor is not None:
        # Same cascade _checkout_picker_vendor_fleet already used — explicit
        # truck/picker-warehouse tie -> vendor's owning company — extended
        # with one more step below (region match), each only narrowing the
        # pool when it actually has candidates (fail-open).
        assigned_truck_ids = get_truck_ids_for_warehouse(vendor_type, vendor_id)
        assigned_picker_ids = get_picker_ids_for_warehouse(vendor_type, vendor_id)
        if assigned_truck_ids or assigned_picker_ids:
            narrowed = [c for c in all_candidates if c.get('truck_id') in assigned_truck_ids or c['user_id'] in assigned_picker_ids]
            if narrowed:
                all_candidates = narrowed
        if vendor.get('company_id'):
            narrowed = [c for c in all_candidates if c.get('company_id') == vendor['company_id']]
            if narrowed:
                all_candidates = narrowed

    # Region cascade — see the plan's "Region-aware candidate pool" note.
    # Same-region candidates, when any exist, are the only ones offered
    # (and can confidently commit to the customer's chosen date); an
    # interregional candidate only ever appears when no same-region one
    # exists at all, keeping B2B's interregional fallback intact.
    delivery_region_id = resolve_region_for_location(checkout['lat'], checkout['lng'])
    if delivery_region_id:
        same_region = [c for c in all_candidates if c.get('region_id') == delivery_region_id]
        if same_region:
            all_candidates = same_region

    if not all_candidates:
        return [], None

    candidate_ids = [c['user_id'] for c in all_candidates]
    load = get_active_order_load_batch(candidate_ids)
    rating_summaries = get_picker_rating_summary_batch(candidate_ids)
    for c in all_candidates:
        c['today_count'] = load.get(c['user_id'], 0)

    return group_and_rank_providers(all_candidates, rating_summaries, platform_avg_rating)[:3], None


@role_required('customer')
def checkout_provider(request):
    """New checkout step for B2B/vendor-fleet carts — select a fleet
    COMPANY (which decides internally which of its own drivers/trucks
    takes the job) or an unaffiliated individual owner-operator, BEFORE
    picking a delivery date (different companies may run different
    delivery calendars — see checkout_schedule). Plain B2C checkout never
    reaches this view (see _needs_provider_step)."""
    checkout = request.session.get('checkout')
    if not checkout or 'lat' not in checkout:
        return redirect('market:checkout_location')

    customer_id = request.session['picker_user']['id']
    items, subtotal = _cart_items_and_subtotal(customer_id, _account_type(request), lang=request.session.get('language', 'en'))
    if not _needs_provider_step(request, customer_id):
        # A B2C session landing here directly (stale link) — nothing to
        # choose, go straight to the normal B2C flow.
        return redirect('market:checkout_schedule')

    t = get_t(request)
    vendor_ties = _required_vendor_ties(items)
    vendor_type = vendor_id = None
    vendor = None
    if vendor_ties:
        if len(vendor_ties) > 1:
            messages.error(request, t['msg_mixed_vendor_cart'])
            return redirect('market:cart')
        vendor_type, vendor_id = next(iter(vendor_ties))
        if any((item['product'].get('vendor_type'), item['product'].get('vendor_id')) != (vendor_type, vendor_id) for item in items):
            messages.error(request, t['msg_vendor_cart_no_mixing'])
            return redirect('market:cart')
        from .vendors import get_vendor
        vendor = get_vendor(vendor_type, vendor_id)
    else:
        required_market_ids = list(_required_market_ids(items))
        if len(required_market_ids) >= 2 and get_multi_stop_pickup_enabled():
            pass
        elif len(required_market_ids) > 1:
            messages.error(request, t['msg_mixed_market_cart'])
            return redirect('market:cart')
        elif len(required_market_ids) == 1:
            # A B2B cart tied to exactly one market — same waypoint/
            # narrowing treatment as a warehouse/industry tie (see
            # get_truck_ids_for_warehouse/get_picker_ids_for_warehouse in
            # _gather_b2b_candidates, both already vendor-type-agnostic).
            # Without this, a market-sourced B2B order priced as if the
            # truck started at the CUSTOMER's own address, skipping the
            # pickup leg entirely, and any truck/picker admin explicitly
            # tied to this market at market/fleet.py::set_truck_warehouses/
            # set_picker_warehouses was silently never preferred.
            vendor_type, vendor_id = 'market', required_market_ids[0]
            vendor = get_market(vendor_id)

    total_weight_kg = _total_weight_kg(items)
    picking_fee = calculate_picking_fee(total_weight_kg, _distinct_items_by_category(items))
    package_fee = calculate_package_fee(total_weight_kg, _total_quantity(items))
    if vendor and vendor_type != 'market':
        _, picking_fee = apply_vendor_fee_waivers(vendor, total_weight_kg, None, picking_fee)
    options, error_key = _gather_b2b_candidates(checkout, total_weight_kg, vendor_type, vendor_id)
    if error_key:
        messages.error(request, t[error_key])
        return redirect('market:cart')

    selected_key = request.POST.get('provider_key')
    if request.method == 'POST' and selected_key:
        option = next((o for o in options if f"{'company' if o['is_company'] else 'individual'}:{o.get('company_id') or o['user_id']}" == selected_key), None)
        if not option:
            messages.error(request, t['msg_truck_unavailable'])
            return redirect('market:checkout_provider')

        truck_type = next((tt for tt in get_active_truck_types(segment='b2b') if tt['id'] == option['truck_type_id']), None)
        origin_lat, origin_lng = (float(vendor['lat']), float(vendor['lng'])) if vendor else (checkout['lat'], checkout['lng'])
        waypoints = [(origin_lat, origin_lng)] if vendor else []
        route = get_multi_stop_route(
            option['lat'], option['lng'], waypoints, checkout['lat'], checkout['lng'],
            transport_mode='truck', truck_weight_kg=truck_type['max_weight_kg'] if truck_type else None,
        )
        distance_km = route['distance_km'] if route else haversine_km(checkout['lat'], checkout['lng'], option['lat'], option['lng'])
        pickup_distance_km, delivery_distance_km = split_pickup_delivery_distance(route) if route else (None, None)

        # Deferred to this confirmation moment, not shown while browsing the
        # options above -- Overpass is a slow, rate-limited public service
        # (same principle checkout_picker's own POST handler already uses
        # for a B2C picker). Feeds the far-order delivery estimate below.
        route_surface = get_route_surface_summary(route['geometry']) if route else None

        from .fleet import get_company_for_picker
        company = get_company_for_picker(option['user_id']) if option['is_company'] else None

        checkout.update({
            'provider_type': 'company' if option['is_company'] else 'individual',
            'company_id': option.get('company_id') if option['is_company'] else None,
            'picker_id': None if option['is_company'] else option['user_id'],
            'picker_name': company['company_name'] if company else option['name'],
            'picker_lat': None if option['is_company'] else option['lat'],
            'picker_lng': None if option['is_company'] else option['lng'],
            'truck_type_id': option['truck_type_id'],
            'distance_km': round(distance_km, 2),
            'pickup_distance_km': pickup_distance_km,
            'delivery_distance_km': delivery_distance_km,
            'route_surface_label': route_surface['label'] if route_surface else None,
            'route_paved_pct': route_surface['paved_pct'] if route_surface else None,
            'route_picker_ok': route_surface['picker_ok'] if route_surface else None,
            'eta_traffic_min': None,
            'picker_instructions': request.POST.get('picker_instructions', '').strip(),
            # Deliberately NOT stored for a market tie ('market' just above)
            # -- this pair specifically drives _make_vendor_fleet_after_create
            # in checkout_schedule (real-stock decrement + pickker_order_vendors
            # tie + the admin-payment-gate on the picker pickup UI), all of
            # which are warehouse/industry-only concepts. A market already has
            # its own, separate shop-payment system (market/shop_payments.py,
            # wired in via required_market_ids, not this pair) -- writing
            # 'market' here would double-track the same order under both
            # systems and wrongly gate the picker behind a vendor payment
            # that was never meant to apply to a market order.
            'vendor_type': vendor_type if vendor_type != 'market' else None,
            'vendor_id': vendor_id if vendor_type != 'market' else None,
        })
        request.session['checkout'] = checkout
        request.session.modified = True
        save_checkout_state(customer_id, checkout)
        return redirect('market:checkout_schedule')

    # Build display-only estimates (route/fee) for each option — these are
    # estimates only; price is locked at whatever's shown here once the
    # customer confirms a date at checkout_schedule (this session's standing
    # "never recompute price after the fact" rule).
    from .fleet import get_company_for_picker
    delivery_region_id = resolve_region_for_location(checkout['lat'], checkout['lng'])
    display_options = []
    for option in options:
        truck_type = next((tt for tt in get_active_truck_types(segment='b2b') if tt['id'] == option['truck_type_id']), None)
        origin_lat, origin_lng = (float(vendor['lat']), float(vendor['lng'])) if vendor else (checkout['lat'], checkout['lng'])
        waypoints = [(origin_lat, origin_lng)] if vendor else []
        route = get_multi_stop_route(
            option['lat'], option['lng'], waypoints, checkout['lat'], checkout['lng'],
            transport_mode='truck', truck_weight_kg=truck_type['max_weight_kg'] if truck_type else None,
        )
        distance_km = route['distance_km'] if route else haversine_km(checkout['lat'], checkout['lng'], option['lat'], option['lng'])
        company = get_company_for_picker(option['user_id']) if option['is_company'] else None
        estimated_fee = calculate_b2b_delivery_fee(distance_km, truck_type, is_quick=False, total_weight_kg=total_weight_kg) if truck_type else None
        if vendor and vendor_type != 'market' and estimated_fee is not None:
            estimated_fee, _ = apply_vendor_fee_waivers(vendor, total_weight_kg, estimated_fee, picking_fee)
        display_options.append({
            'key': f"{'company' if option['is_company'] else 'individual'}:{option.get('company_id') or option['user_id']}",
            'is_company': option['is_company'],
            'name': company['company_name'] if company else option['name'],
            'truck_type': truck_type,
            'rating_display': option.get('rating_display'),
            'distance_km': round(distance_km, 2),
            'estimated_total': (subtotal + estimated_fee + picking_fee + package_fee) if estimated_fee is not None else None,
            'free_delivery': estimated_fee == Decimal('0') if estimated_fee is not None else False,
            'same_region': bool(delivery_region_id) and option.get('region_id') == delivery_region_id,
            'lat': option['lat'],
            'lng': option['lng'],
            'route_geometry': route['geometry'] if route else None,
        })

    # A route map for the top-ranked (best) option only, not a live
    # per-option switcher -- matches this app's established "one
    # auto-suggested option" pattern (see the fair-picker-ranking
    # algorithm) rather than a new interactive multi-map feature.
    top_option = display_options[0] if display_options else None
    route_geometry_json = json.dumps(top_option['route_geometry']) if top_option and top_option.get('route_geometry') else None

    return render(request, 'market/checkout_provider.html', {
        'options': display_options,
        'checkout': checkout,
        'subtotal': subtotal,
        'total_weight_kg': total_weight_kg,
        'is_vendor_fleet': bool(vendor_ties),
        'vendor': vendor,
        'top_option': top_option,
        'route_geometry_json': route_geometry_json,
    })


@role_required('customer')
def checkout_picker(request):
    """Final checkout step for plain B2C carts only — B2B/vendor-fleet
    carts now finish through checkout_provider -> checkout_schedule
    instead (see _needs_provider_step). A B2B/vendor-tied session that
    somehow lands here (stale bookmark, back-button) is bounced to the
    right place rather than silently falling through to the old per-driver
    logic that used to live here."""
    checkout = request.session.get('checkout')
    if not checkout or 'lat' not in checkout:
        return redirect('market:checkout_location')
    if 'delivery_date' not in checkout:
        return redirect('market:checkout_schedule')

    customer_id = request.session['picker_user']['id']
    account_type = _account_type(request)
    items, subtotal = _cart_items_and_subtotal(customer_id, account_type, lang=request.session.get('language', 'en'))

    if _needs_provider_step(request, customer_id):
        return redirect('market:checkout_provider')

    required_market_ids = list(_required_market_ids(items))
    if len(required_market_ids) >= 2 and get_multi_stop_pickup_enabled():
        # Admin has opted into multi-stop pickups — a picker can visit every
        # required market as a stop on the one trip instead of this being
        # blocked, however many there are.
        pass
    elif len(required_market_ids) > 1:
        # Two or more items each unique to a different market, and
        # multi-stop pickups aren't turned on — no single picker can fetch
        # all of them, so this can't be one order.
        messages.error(request, get_t(request)['msg_mixed_market_cart'])
        return redirect('market:cart')

    # A single-vendor cart only ever reaches this view (instead of
    # checkout_provider) when _needs_provider_step already confirmed it's a
    # close-zone, minimum-met order — see the carve-out there. Resolve the
    # same vendor here so _checkout_picker_b2c can route candidates through
    # it and record the tie/stock decrement on order creation.
    vendor_ties = _required_vendor_ties(items)
    vendor = vendor_type = vendor_id = None
    if vendor_ties:
        vendor_type, vendor_id = next(iter(vendor_ties))
        from .vendors import get_vendor
        vendor = get_vendor(vendor_type, vendor_id)

    picking_fee = calculate_picking_fee(_total_weight_kg(items), _distinct_items_by_category(items))
    package_fee = calculate_package_fee(_total_weight_kg(items), _total_quantity(items))
    if vendor:
        _, picking_fee = apply_vendor_fee_waivers(vendor, _total_weight_kg(items), None, picking_fee)
    return _checkout_picker_b2c(
        request, checkout, customer_id, items, subtotal, picking_fee, checkout['delivery_date'], package_fee,
        required_market_ids, vendor=vendor, vendor_type=vendor_type, vendor_id=vendor_id,
    )


def _checkout_picker_b2c(request, checkout, customer_id, items, subtotal, picking_fee, delivery_date, package_fee=0, required_market_ids=None, vendor=None, vendor_type=None, vendor_id=None):
    required_market_ids = required_market_ids or []
    client = get_client()
    query = client.table('pickker_picker_profiles').select('*').eq('is_approved', True).eq('picker_segment', 'b2c')
    if len(required_market_ids) == 1:
        # Exactly one cart item is unique to this one market — only a
        # picker actually registered there can fetch it (see
        # market/views.py::_required_market_ids).
        query = query.eq('market_id', required_market_ids[0])
    # len >= 2 (multi-stop pickup, admin opt-in) deliberately doesn't filter
    # here — any available, reasonably-positioned picker can be sent to
    # visit every required market as a stop, not only one already
    # registered at one of them.
    profiles_resp = query.execute()
    profiles = [
        p for p in profiles_resp.data
        if p.get('market_id') or (p.get('location_lat') is not None and p.get('location_lng') is not None)
    ]

    # B2C is region-locked (B2B stays inter-regional — see
    # _checkout_picker_b2b, which has no equivalent filter): a picker with a
    # registered region can only be offered a delivery whose resolved
    # region matches theirs. This is what actually prevents "1kg of fish
    # ordered from a picker two regions away" — not a minimum-order rule.
    # Deliberately fails open (never excludes anyone) whenever a picker has
    # no region assigned yet, or the delivery location's region can't be
    # resolved — so rollout never silently breaks a picker/area admin
    # hasn't tagged yet. See market/regions.py.
    from .regions import resolve_region_for_location
    delivery_region_id = resolve_region_for_location(checkout.get('lat'), checkout.get('lng'))
    if delivery_region_id:
        profiles = [p for p in profiles if not p.get('region_id') or p['region_id'] == delivery_region_id]

    # Weight-based eligibility — same principle B2B truck matching already
    # has (_gather_b2b_candidates): a picker whose registered vehicle can't
    # actually carry this cart shouldn't be offered for it. Fails open,
    # matching this app's established pattern for incomplete data — a
    # picker with no truck_type_id set (never migrated, or admin removed
    # their vehicle type) is never excluded, only one whose vehicle's real
    # max_weight_kg is known and too small.
    from .truck_types import get_active_truck_types
    b2c_truck_types_by_id = {tt['id']: tt for tt in get_active_truck_types(segment='b2c')}
    total_weight_kg = _total_weight_kg(items)
    profiles = [
        p for p in profiles
        if not p.get('truck_type_id')
        or not b2c_truck_types_by_id.get(p['truck_type_id'])
        or float(b2c_truck_types_by_id[p['truck_type_id']]['max_weight_kg']) >= float(total_weight_kg)
    ]

    picker_ids = [p['user_id'] for p in profiles]
    users_by_id = {}
    if picker_ids:
        # Filtered to is_active=True so a suspended picker (however available/
        # approved their profile still looks) can never be assigned a new
        # delivery — the loop below skips any profile with no matching entry here.
        users_resp = client.table('pickker_users').select('id, first_name, last_name').in_('id', picker_ids).eq('is_active', True).execute()
        users_by_id = {u['id']: u for u in users_resp.data}

    # Pickers already out on an active delivery near this new destination get
    # a lead over an otherwise-closer picker with nothing nearby — batching
    # same-direction orders onto one picker (who can then work them via the
    # multi-order route view) beats sending a second picker across town.
    SAME_ROUTE_RADIUS_KM = 3.0
    active_orders_resp = (
        client.table('pickker_orders')
        .select('picker_id, delivery_lat, delivery_lng')
        .in_('picker_id', picker_ids)
        .in_('status', ['confirmed', 'picking', 'in_transit'])
        .execute()
    ) if picker_ids else None
    active_stops_by_picker = {}
    if active_orders_resp:
        for order in active_orders_resp.data:
            if order.get('delivery_lat') is None:
                continue
            active_stops_by_picker.setdefault(order['picker_id'], []).append(
                (float(order['delivery_lat']), float(order['delivery_lng'])),
            )

    max_orders_per_day = get_max_orders_per_day()
    # Batched — one query for every candidate's daily order count instead of
    # one query per candidate (see market/picker_availability.py). This is
    # also what used to be the availability check inside get_picker_status;
    # duplicating the comparison here rather than calling that per-picker
    # keeps this whole function to a fixed, small number of queries no
    # matter how large the picker pool grows.
    order_counts = get_order_counts_for_date_batch(picker_ids, delivery_date)

    pickers = []
    for p in profiles:
        user = users_by_id.get(p['user_id'])
        if not user:
            continue

        # Distance/fee is always anchored to the picker's registered market
        # (or, for a warehouse/shop-sourced picker with no market, their
        # profile pin) — never wherever they physically are right now.
        # Otherwise a picker mid-delivery on another order would price a new
        # order as if starting from the middle of that route, when in
        # reality they still have to swing back to their market to pick it.
        origin_lat, origin_lng = get_picker_origin_coords(p)
        if origin_lat is None:
            continue

        # Availability is fully automatic — never a manually-toggled flag.
        # The only thing that excludes a picker from a new request is having
        # already hit their daily order cap; being out delivering something
        # else never blocks it — they can swing back to their market to
        # pick this one up too.
        today_count = order_counts.get(p['user_id'], 0)
        if today_count >= max_orders_per_day:
            continue

        distance = haversine_km(checkout['lat'], checkout['lng'], origin_lat, origin_lng)
        nearby_active_km = None
        for stop_lat, stop_lng in active_stops_by_picker.get(p['user_id'], []):
            stop_distance = haversine_km(checkout['lat'], checkout['lng'], stop_lat, stop_lng)
            if nearby_active_km is None or stop_distance < nearby_active_km:
                nearby_active_km = stop_distance
        same_route = nearby_active_km is not None and nearby_active_km <= SAME_ROUTE_RADIUS_KM

        picker_truck_type = b2c_truck_types_by_id.get(p.get('truck_type_id'))
        pickers.append({
            'user_id': p['user_id'],
            'name': f"{user['first_name']} {user['last_name']}".strip() or 'Picker',
            'market_location': p.get('market_location', ''),
            'source_type': p.get('source_type', 'market'),
            'vehicle_type': p.get('vehicle_type') or 'car',
            'truck_icon': picker_truck_type.get('icon') if picker_truck_type else None,
            'truck_icon_url': picker_truck_type.get('icon_url') if picker_truck_type else None,
            'lat': origin_lat,
            'lng': origin_lng,
            'distance_km': round(distance, 2),
            'today_count': today_count,
            'same_route': same_route,
            'same_route_distance_km': round(nearby_active_km, 2) if nearby_active_km is not None else None,
        })

    # Fair, rating-aware ranking — proximity, how loaded each picker already
    # is that day, and their rating combined into one score, rather than a
    # strict "nearest wins" sort. See market/picker_ranking.py for why.
    rating_summaries = get_picker_rating_summary_batch(picker_ids)
    platform_avg_rating = get_platform_rating_summary().get('average')
    pickers = rank_pickers(pickers, rating_summaries, platform_avg_rating)

    # The raw distance here is a straight-line (haversine) estimate, not a
    # real road distance — showing it as a number to the customer invites
    # reading it as the actual trip length.
    if pickers:
        nearest_km = min(p['distance_km'] for p in pickers)
        for p in pickers:
            p['distance_tag'] = 'closest' if p['distance_km'] <= nearest_km * 1.3 else 'farther'

    def _route_for_candidate(candidate):
        """Real route (+ mixed-order suggested-market, if applicable) for
        one candidate picker — same logic that used to run once, for
        whichever picker the customer clicked. Now run for a handful of
        top-ranked candidates so the final pick can be refined by real
        traffic-adjusted travel time, not just the composite score."""
        # Market-driven waypoints, built exactly as before (unchanged) --
        # then the vendor stop is appended on top, separately, rather than
        # living in the same elif chain. A cart CAN mix a market-tied item
        # with a warehouse/industry-tied item (that exclusivity only holds
        # per PRODUCT, not per cart) -- the old elif chain let the
        # market-suggestion branch silently win in that combination,
        # dropping the vendor stop from the route/fee entirely (the
        # stock-decrement/vendor-tie recording below was never affected by
        # this -- only the route itself). get_multi_stop_route picks its
        # own sensible visiting order for whatever stops end up in this
        # list, so append order here doesn't matter.
        cand_suggested_market = None
        waypoints = []
        if len(required_market_ids) >= 2:
            # Multi-stop pickup — real route through every required market,
            # not a direct picker-to-customer trip.
            waypoints = [(float(m['lat']), float(m['lng'])) for m in (get_market(mid) for mid in required_market_ids) if m]
        elif len(required_market_ids) == 1 and _has_generic_items(items):
            # Mixed order — one item unique to a market, but the cart also
            # has plain/generic items with nowhere specific to come from.
            # Suggest the market nearest this candidate as an extra stop.
            nearest = get_nearest_market(candidate['lat'], candidate['lng'])
            if nearest and nearest['id'] != required_market_ids[0]:
                cand_suggested_market = nearest
                tied_market = get_market(required_market_ids[0])
                waypoints = [(float(tied_market['lat']), float(tied_market['lng'])), (float(nearest['lat']), float(nearest['lng']))]
            # else: nearest IS the tied market (or none found) — no market
            # waypoint needed, candidate['lat']/['lng'] already IS that
            # market's own location (see get_picker_origin_coords), same as
            # before this fix.

        if vendor is not None:
            # Close-zone warehouse/industry order routed to a regular B2C
            # picker (see _needs_provider_step's carve-out) — the picker
            # must still swing by the vendor to collect the goods before
            # heading to the customer, so the shortlist's real route (and
            # the fee it bills) has to include that leg, same waypoint
            # pattern _gather_b2b_candidates/checkout_provider already use
            # for the truck/company path.
            waypoints.append((float(vendor['lat']), float(vendor['lng'])))

        if waypoints:
            cand_route = get_multi_stop_route(candidate['lat'], candidate['lng'], waypoints, checkout['lat'], checkout['lng'])
        else:
            cand_route = get_route(candidate['lat'], candidate['lng'], checkout['lat'], checkout['lng'])
        return candidate, cand_route, cand_suggested_market

    selected_picker = None
    route = None
    suggested_market = None
    if pickers:
        # Only the top few candidates get a real (network-call) route
        # lookup — computed in parallel so this costs roughly one call's
        # worth of latency, not several, regardless of how many candidates
        # are in the running. Re-ranking just this shortlist by real
        # traffic-adjusted ETA is what actually answers "who can deliver on
        # time," which raw distance alone can't.
        from concurrent.futures import ThreadPoolExecutor
        top_candidates = pickers[:3]
        results = []
        with ThreadPoolExecutor(max_workers=len(top_candidates)) as executor:
            for candidate, cand_route, cand_suggested_market in executor.map(_route_for_candidate, top_candidates):
                if cand_route:
                    eta = traffic_adjusted_duration(cand_route['duration_min'])
                    results.append((eta if eta is not None else float('inf'), candidate, cand_route, cand_suggested_market))
        if results:
            results.sort(key=lambda r: r[0])
            _, selected_picker, route, suggested_market = results[0]
        else:
            # Every candidate's route lookup failed (routing API down) —
            # fall back to the top-ranked pick with an estimated route so
            # checkout still works, degraded but not broken.
            selected_picker = pickers[0]
            route = {
                'distance_km': selected_picker['distance_km'],
                'duration_min': None,
                'geometry': None,
            }

    eta_with_buffer = traffic_adjusted_duration(route['duration_min']) if route else None
    pickup_distance_km, delivery_distance_km = split_pickup_delivery_distance(route) if route else (None, None)

    if request.method == 'POST' and selected_picker and route:
        if not has_capacity_for_date(selected_picker['user_id'], delivery_date):
            # Re-checked here since the picker could have filled up their
            # quota for this exact date between page load and submit.
            messages.error(request, get_t(request)['msg_picker_fully_booked'].format(picker_name=selected_picker['name'], delivery_date=delivery_date))
            return redirect('market:checkout_picker')
        # No separate vendor re-check needed here: checkout_picker (the
        # view) already re-runs _needs_provider_step fresh on every single
        # request — GET or POST alike — before ever delegating down to this
        # function, so a cart that fell out of close-zone eligibility
        # between page load and this submit is redirected to
        # checkout_provider before it gets here at all. A vendor reaching
        # this point is therefore guaranteed close-zone-and-eligible as of
        # THIS request. See checkout_schedule's provider branch for where
        # the equivalent re-check genuinely is needed (that flow, once
        # provider_type is set, does NOT re-verify on every request).
        # The road-surface check hits a shared public API (Overpass) that can
        # be slow — only run it here, at the moment of actually confirming
        # the order, not on every page render while browsing pickers (that
        # was what made picker selection feel slow: the whole page waited on
        # it just to draw the map). The GET render below fetches it async
        # instead, well after the map's already visible.
        surface_summary = get_route_surface_summary(route['geometry']) if route.get('geometry') else None
        from .truck_types import get_per_km_rate_for_picker
        fee = calculate_delivery_fee(route['distance_km'], is_quick=checkout.get('is_quick', False), per_km_rate=get_per_km_rate_for_picker(selected_picker.get('user_id')))
        if vendor:
            fee, picking_fee = apply_vendor_fee_waivers(vendor, _total_weight_kg(items), fee, picking_fee)
        checkout.update({
            'picker_id': selected_picker['user_id'],
            'picker_name': selected_picker['name'],
            'picker_lat': selected_picker['lat'],
            'picker_lng': selected_picker['lng'],
            'distance_km': route['distance_km'],
            'pickup_distance_km': pickup_distance_km,
            'delivery_distance_km': delivery_distance_km,
            'route_surface_label': surface_summary['label'] if surface_summary else None,
            'route_paved_pct': surface_summary['paved_pct'] if surface_summary else None,
            'eta_traffic_min': eta_with_buffer,
            'picker_instructions': request.POST.get('picker_instructions', '').strip(),
            'suggested_market_id': suggested_market['id'] if suggested_market else None,
            'vendor_type': vendor_type,
            'vendor_id': vendor_id,
        })
        after_create = _make_vendor_fleet_after_create(request, items, vendor_type, vendor_id) if vendor is not None else None
        return _create_order_and_finish(request, checkout, customer_id, items, subtotal, fee, picking_fee, package_fee=package_fee, after_create=after_create)

    multi_stop_markets = []
    if len(required_market_ids) >= 2:
        multi_stop_markets = [m for m in (get_market(mid) for mid in required_market_ids) if m]

    from .truck_types import get_per_km_rate_for_picker
    delivery_fee = calculate_delivery_fee(route['distance_km'], is_quick=checkout.get('is_quick', False), per_km_rate=get_per_km_rate_for_picker(selected_picker.get('user_id')) if selected_picker else None) if route else None
    if vendor and delivery_fee is not None:
        delivery_fee, _ = apply_vendor_fee_waivers(vendor, _total_weight_kg(items), delivery_fee, picking_fee)
    return render(request, 'market/checkout_picker.html', {
        'is_b2b': False,
        'pickers': pickers,
        'selected_picker': selected_picker,
        'multi_stop_markets': multi_stop_markets,
        'suggested_market': suggested_market,
        'checkout': checkout,
        'subtotal': subtotal,
        'vendor': vendor,
        'total_weight_kg': _total_weight_kg(items),
        'delivery_fee': delivery_fee,
        'picking_fee': picking_fee,
        'package_fee': package_fee,
        'combined_fee': (delivery_fee + picking_fee + package_fee) if delivery_fee is not None else None,
        'total': (subtotal + delivery_fee + picking_fee + package_fee) if delivery_fee is not None else None,
        'route_duration': route['duration_min'] if route else None,
        'route_distance': route['distance_km'] if route else None,
        'pickup_distance_km': pickup_distance_km,
        'delivery_distance_km': delivery_distance_km,
        'route_geometry_json': json.dumps(route['geometry']) if route and route.get('geometry') else None,
        'eta_with_buffer': eta_with_buffer,
    })


@role_required('customer')
def order_list(request):
    picker_user = request.session['picker_user']
    resp = (
        get_client()
        .table('pickker_orders')
        .select('*')
        .eq('customer_id', picker_user['id'])
        .order('created_at', desc=True)
        .execute()
    )
    orders = resp.data
    if orders:
        mark_all_orders_seen(picker_user['id'], orders, request=request)
    t = get_t(request)
    for order in orders:
        order['tracker_step_index'] = current_step_index(order['status'])
        order['status_label'] = get_status_label(t, order['status'])
    return render(request, 'market/order_list.html', {'orders': orders, 'tracker_steps': get_tracker_steps(t)})


@role_required('customer')
@require_POST
def reorder(request, order_id):
    """Re-adds every item from a past order back into the cart in one click
    — the main practical win for B2B restocking, but works for any customer."""
    picker_user = request.session['picker_user']
    client = get_client()
    order_resp = client.table('pickker_orders').select('id').eq('id', order_id).eq('customer_id', picker_user['id']).execute()
    if not order_resp.data:
        raise Http404

    items_resp = client.table('pickker_order_items').select('product_id, quantity').eq('order_id', order_id).execute()
    added_count = 0
    for item in items_resp.data:
        cart_utils.add_to_cart(picker_user['id'], item['product_id'], item['quantity'])
        added_count += 1

    if added_count:
        messages.success(request, get_t(request)['msg_reorder_added'].format(added_count=added_count, order_id=order_id))
    else:
        messages.error(request, get_t(request)['msg_reorder_not_found'])
    return redirect('market:cart')


@role_required('customer')
def order_detail(request, order_id):
    picker_user = request.session['picker_user']
    client = get_client()
    resp = client.table('pickker_orders').select('*').eq('id', order_id).execute()
    order = resp.data[0] if resp.data else None
    if not order or order['customer_id'] != picker_user['id']:
        raise Http404
    mark_order_seen(picker_user['id'], order['id'], order['status'], request=request)
    mark_messages_seen(picker_user['id'], order['id'])

    items_resp = client.table('pickker_order_items').select('*').eq('order_id', order_id).execute()

    # Same as the picker's order view — the unit/measure snapshot is
    # immutable, but its displayed label should track whichever language
    # this customer has the site set to right now, not whatever was active
    # when they placed the order.
    lang = request.session.get('language', 'en')
    unit_label_map = get_unit_label_map(lang)
    product_display_names = get_product_display_names((item.get('product_id') for item in items_resp.data), lang)
    product_badges = get_product_badges(item.get('product_id') for item in items_resp.data)
    for item in items_resp.data:
        key = item.get('measure_type') or item.get('unit')
        item['unit_display'] = unit_label_map.get(key, item.get('measure_label') or item.get('unit'))
        item['per_unit_text'] = per_each_unit_display(key, lang)
        item['qty_unit_display'] = quantity_unit_display(item['quantity'], key, lang)
        item['product_display_name'] = product_display_names.get(item.get('product_id'), item.get('product_name'))
        badge = product_badges.get(item.get('product_id'), {})
        item['market_name'] = badge.get('market_name')
        item['market_logo_url'] = badge.get('market_logo_url')
        item['quality_grade'] = badge.get('quality_grade')

    picker_name = None
    picker_vehicle_type = None
    picker_truck_icon = None
    picker_truck_icon_url = None
    if order.get('picker_id'):
        from market.truck_types import get_picker_vehicle_display

        picker_resp = client.table('pickker_users').select('first_name,last_name').eq('id', order['picker_id']).execute()
        if picker_resp.data:
            p = picker_resp.data[0]
            picker_name = f"{p['first_name']} {p['last_name']}".strip()
        vehicle_display = get_picker_vehicle_display(order['picker_id'])
        picker_vehicle_type = vehicle_display['vehicle_type']
        picker_truck_icon = vehicle_display['truck_icon']
        picker_truck_icon_url = vehicle_display['truck_icon_url']

    whatsapp_link = None
    if order['status'] == 'pending_payment':
        customer_resp = client.table('pickker_users').select('first_name,last_name,phone_number').eq('id', picker_user['id']).execute()
        customer = customer_resp.data[0] if customer_resp.data else {}
        customer_name = f"{customer.get('first_name', '')} {customer.get('last_name', '')}".strip() or picker_user.get('first_name', '')
        whatsapp_link = build_whatsapp_proof_link(
            order_id, order['total_amount'], customer_name, customer.get('phone_number', ''), picker_user['email'],
        )

    from market.shop_payments import get_required_markets_with_status
    remaining_markets = [m for m in get_required_markets_with_status(order_id) if not m['picked_up']]

    return render(request, 'market/order_detail.html', {
        'order': order,
        'items': items_resp.data,
        'picker_name': picker_name,
        'picker_vehicle_type': picker_vehicle_type,
        'picker_truck_icon': picker_truck_icon,
        'picker_truck_icon_url': picker_truck_icon_url,
        'remaining_markets': remaining_markets,
        'whatsapp_link': whatsapp_link,
        'tracker_steps': get_tracker_steps(get_t(request)),
        'current_step_index': current_step_index(order['status']),
        'payment_methods': get_active_payment_methods() if order['status'] == 'pending_payment' else [],
        'messages_thread': get_messages(order_id) if order.get('picker_id') else [],
        'my_user_id': picker_user['id'],
        'existing_rating': get_rating_for_order(order_id) if order['status'] == 'delivered' else None,
    })


@role_required('customer')
def order_invoice(request, order_id):
    """In-app invoice for a B2B order — the customer can revisit this any
    time from order_detail (unlike send_b2b_invoice's one-shot email at
    order creation), with Clanert Sustain's own payment details and the
    payment-held notice. Same ownership check as order_detail; B2B only
    since B2C orders never had a formal invoice concept."""
    picker_user = request.session['picker_user']
    client = get_client()
    resp = client.table('pickker_orders').select('*').eq('id', order_id).execute()
    order = resp.data[0] if resp.data else None
    if not order or order['customer_id'] != picker_user['id'] or order.get('account_type') != 'b2b':
        raise Http404

    items_resp = client.table('pickker_order_items').select('*').eq('order_id', order_id).execute()
    truck_type = get_truck_type(order.get('truck_type_id')) if order.get('truck_type_id') else None
    business_name, items = get_b2b_invoice_data(order, items_resp.data, truck_type)

    return render(request, 'market/order_invoice.html', {
        'order': order,
        'business_name': business_name,
        'items': items,
        'truck_type': truck_type,
        'payment_methods': get_active_payment_methods(),
    })


@role_required('customer')
def order_track_location(request, order_id):
    """Polled from the customer's order-detail page while the order is
    in_transit, so they can watch the picker's live position — and the real
    road route to their door, not a straight line — on a small map. The
    customer-side counterpart to the picker's live navigation view."""
    from .geo import get_multi_stop_route
    from .live_tracking import get_live_location
    from .shop_payments import get_remaining_pickup_waypoints

    picker_user = request.session['picker_user']
    resp = get_client().table('pickker_orders').select('customer_id, delivery_lat, delivery_lng').eq('id', order_id).execute()
    order = resp.data[0] if resp.data else None
    if not order or order.get('customer_id') != picker_user['id']:
        raise Http404

    location = get_live_location(order_id)
    route = None
    if location:
        remaining_waypoints = get_remaining_pickup_waypoints(order_id)
        route = get_multi_stop_route(location['lat'], location['lng'], remaining_waypoints, float(order['delivery_lat']), float(order['delivery_lng']))

    return JsonResponse({
        'location': location,
        'route': {
            'distance_km': route['distance_km'],
            'duration_min': route['duration_min'],
            'geometry': route['geometry'],
        } if route else None,
    })


@role_required('customer')
@require_POST
def order_send_message(request, order_id):
    picker_user = request.session['picker_user']
    client = get_client()
    resp = client.table('pickker_orders').select('*').eq('id', order_id).execute()
    order = resp.data[0] if resp.data else None
    if order and order['customer_id'] == picker_user['id']:
        message = send_message(order_id, picker_user['id'], request.POST.get('message', ''))
        if message:
            mark_messages_seen(picker_user['id'], order_id)
            notify_new_message(order, 'customer', message['message'])
            push_new_message(order, 'customer', message['message'])
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        return JsonResponse({'messages': get_messages(order_id)})
    return redirect('market:order_detail', order_id=order_id)


@role_required('customer')
def order_messages_poll(request, order_id):
    picker_user = request.session['picker_user']
    client = get_client()
    resp = client.table('pickker_orders').select('customer_id, status').eq('id', order_id).execute()
    if not resp.data or resp.data[0]['customer_id'] != picker_user['id']:
        raise Http404
    mark_messages_seen(picker_user['id'], order_id)
    return JsonResponse({
        'messages': get_messages(order_id),
        'status': resp.data[0]['status'],
        'status_label': get_status_label(get_t(request), resp.data[0]['status']),
    })


@role_required('customer')
@require_POST
def order_submit_rating(request, order_id):
    picker_user = request.session['picker_user']
    client = get_client()
    resp = client.table('pickker_orders').select('customer_id, picker_id, status').eq('id', order_id).execute()
    order = resp.data[0] if resp.data else None
    if order and order['customer_id'] == picker_user['id'] and order['status'] == 'delivered' and order.get('picker_id'):
        rating = request.POST.get('rating')
        if rating and rating.isdigit() and 1 <= int(rating) <= 5:
            submit_rating(order_id, order['picker_id'], picker_user['id'], rating, request.POST.get('comment', ''))
            messages.success(request, get_t(request)['msg_rating_thanks'])
    return redirect('market:order_detail', order_id=order_id)


@role_required('customer')
def saved_plans_list(request):
    customer_id = request.session['picker_user']['id']
    return render(request, 'market/saved_plans.html', {'plans': get_active_plans(customer_id)})


@role_required('customer')
@require_POST
def plan_convert_to_cart(request, plan_id):
    customer_id = request.session['picker_user']['id']
    account_type = _account_type(request)
    plan = get_plan(customer_id, plan_id)
    if plan:
        for item in plan.get('items', []):
            cart_utils.add_to_cart(customer_id, item['product_id'], item['quantity'])
        mark_plan_purchased(customer_id, plan_id)
        messages.success(request, get_t(request)['msg_plan_added_to_cart'].format(plan_title=plan['title']))
    else:
        messages.error(request, get_t(request)['msg_plan_not_found'])
    return redirect('market:cart')


@role_required('customer')
@require_POST
def plan_delete(request, plan_id):
    customer_id = request.session['picker_user']['id']
    delete_plan(customer_id, plan_id)
    messages.success(request, get_t(request)['msg_plan_deleted'])
    return redirect('market:saved_plans')


@role_required('customer')
def spending(request):
    customer_id = request.session['picker_user']['id']
    try:
        days = int(request.GET.get('days', 30))
    except ValueError:
        days = 30
    if days not in (30, 90, 365):
        days = 30

    dashboard = get_spending_dashboard(customer_id, days=days, weeks=8)
    trend = dashboard['trend']
    max_week = max((w['total'] for w in trend), default=0)
    for week in trend:
        week['bar_height_px'] = int(week['total'] / max_week * 120) if max_week else 0

    return render(request, 'market/spending.html', {
        'summary': dashboard['summary'],
        'top_products': dashboard['top_products'],
        'trend': trend,
        'days': days,
    })


@role_required('customer')
def shopping_guide(request):
    """Storage, planning, picker-value, and quantity guidance for customers
    — mirrors the picker's 'How to Pick Best' guide. The banner at the top
    is tailored to whatever category this customer actually buys most
    (reusing the same order-history data as the Spending page), so it's a
    real follow-up/suggestion rather than a generic page everyone sees the
    same way."""
    from .shopping_tips import get_personalized_tip_key

    customer_id = request.session['picker_user']['id']
    tip_key = get_personalized_tip_key(customer_id)
    t = get_t(request)
    return render(request, 'market/shopping_guide.html', {
        'personalized_tip': t.get(tip_key) if tip_key else None,
    })


@role_required('customer')
def my_data(request):
    customer_id = request.session['picker_user']['id']
    history = request.session.get('chat_history')
    if history is None:
        history = load_chat_history(customer_id)
    return render(request, 'market/my_data.html', {
        'chat_message_count': len(history),
        'memory_notes': load_chat_memory(customer_id),
        'plans': get_active_plans(customer_id),
    })


@role_required('customer')
@require_POST
def my_data_clear_chat(request):
    customer_id = request.session['picker_user']['id']
    request.session['chat_history'] = []
    request.session.modified = True
    clear_chat_history(customer_id)
    messages.success(request, get_t(request)['msg_chat_cleared'])
    return redirect('market:my_data')


@role_required('customer')
@require_POST
def my_data_clear_memory(request):
    customer_id = request.session['picker_user']['id']
    clear_chat_memory(customer_id)
    messages.success(request, get_t(request)['msg_memory_cleared'])
    return redirect('market:my_data')


@role_required('customer')
@require_POST
def my_data_delete_plans(request):
    customer_id = request.session['picker_user']['id']
    delete_all_plans(customer_id)
    messages.success(request, get_t(request)['msg_plans_deleted'])
    return redirect('market:my_data')


@role_required('customer')
def profile_edit(request):
    customer_id = request.session['picker_user']['id']
    client = get_client()
    user_resp = client.table('pickker_users').select('*').eq('id', customer_id).execute()
    user_row = user_resp.data[0] if user_resp.data else None
    profile_resp = client.table('pickker_customer_profiles').select('*').eq('user_id', customer_id).execute()
    profile = profile_resp.data[0] if profile_resp.data else None

    if request.method == 'POST':
        first_name = request.POST.get('first_name', '').strip()
        sms_language = request.POST.get('sms_language', '').strip()
        client.table('pickker_users').update({
            'first_name': first_name,
            'last_name': request.POST.get('last_name', '').strip(),
            'phone_number': request.POST.get('phone_number', '').strip(),
            'sms_language': sms_language if sms_language in ('en', 'sw') else (user_row.get('sms_language') if user_row else 'sw'),
        }).eq('id', customer_id).execute()

        lat = request.POST.get('lat')
        lng = request.POST.get('lng')
        business_name = request.POST.get('business_name', '').strip()
        client.table('pickker_customer_profiles').update({
            'business_name': business_name,
            'tin_number': request.POST.get('tin_number', '').strip(),
            'business_license_number': request.POST.get('business_license_number', '').strip(),
            'address': request.POST.get('address', '').strip(),
            'city': request.POST.get('city', '').strip(),
            'area': request.POST.get('area', '').strip(),
            'location_lat': float(lat) if lat else (profile.get('location_lat') if profile else None),
            'location_lng': float(lng) if lng else (profile.get('location_lng') if profile else None),
        }).eq('user_id', customer_id).execute()

        # Keep the session's display name and business_name in sync so both
        # the nav avatar initial and the "My Fleet" nav link's own
        # is_registered_business gate (accounts/context_processors.py)
        # update immediately without needing to log back in.
        picker_user = request.session['picker_user']
        picker_user['first_name'] = first_name
        picker_user['business_name'] = business_name
        request.session['picker_user'] = picker_user
        request.session.modified = True

        messages.success(request, get_t(request)['msg_profile_updated'])
        return redirect('market:profile_edit')

    named_locations = get_named_locations(customer_id)
    t = get_t(request)
    named_location_forms = [
        ('home', {'label': t['profile_home_location_label'], 'location': named_locations['home'], 'save_label': t['profile_save_as_home']}),
        ('business', {'label': t['profile_business_location_label'], 'location': named_locations['business'], 'save_label': t['profile_save_as_business']}),
        ('other', {'label': t['profile_other_location_label'], 'location': named_locations['other'], 'save_label': t['profile_save_as_other']}),
    ]
    return render(request, 'market/profile_edit.html', {
        'user_row': user_row,
        'profile': profile,
        'center': DAR_ES_SALAAM_CENTER,
        'home_location': named_locations['home'],
        'business_location': named_locations['business'],
        'other_location': named_locations['other'],
        'named_location_forms': named_location_forms,
    })


@role_required('customer')
@require_POST
def profile_save_named_location(request, kind):
    from .customer_location import NAMED_LOCATION_KINDS, save_named_location

    if kind not in NAMED_LOCATION_KINDS:
        raise Http404
    customer_id = request.session['picker_user']['id']
    lat = request.POST.get('lat')
    lng = request.POST.get('lng')
    address = request.POST.get('address', '').strip()
    if not lat or not lng:
        messages.error(request, get_t(request)['msg_select_location'])
    else:
        save_named_location(customer_id, kind, address, float(lat), float(lng))
        messages.success(request, get_t(request)['msg_named_location_saved'].format(kind=get_t(request)[f'profile_{kind}_location_label']))
    return redirect('market:profile_edit')


@role_required('customer')
def account_settings(request):
    from .api_auth import get_subscription, list_api_keys

    context = {}
    if _account_type(request) == 'b2b':
        customer_id = request.session['picker_user']['id']
        context['api_keys'] = list_api_keys(customer_id)
        context['new_api_key'] = request.session.pop('new_api_key', None)
        context['api_subscription'] = get_subscription(customer_id)
    return render(request, 'market/account_settings.html', context)


@role_required('customer')
@require_POST
def request_api_subscription_view(request):
    from .api_auth import request_subscription
    from .emailer import notify_admin

    if _account_type(request) != 'b2b':
        raise Http404
    customer_id = request.session['picker_user']['id']
    request_subscription(customer_id, request.POST.get('plan', 'basic'))
    notify_admin(
        'New API subscription request',
        f"Customer #{customer_id} requested an API subscription. Review and activate it from the admin portal.",
    )
    messages.success(request, get_t(request)['msg_api_subscription_requested'])
    return redirect('market:account_settings')


@role_required('customer')
@require_POST
def generate_api_key_view(request):
    from .api_auth import generate_api_key

    if _account_type(request) != 'b2b':
        raise Http404
    customer_id = request.session['picker_user']['id']
    raw_key = generate_api_key(customer_id, request.POST.get('label', ''))
    request.session['new_api_key'] = raw_key
    messages.success(request, get_t(request)['msg_api_key_generated'])
    return redirect('market:account_settings')


@role_required('customer')
@require_POST
def revoke_api_key_view(request, key_id):
    from .api_auth import revoke_api_key

    if _account_type(request) != 'b2b':
        raise Http404
    customer_id = request.session['picker_user']['id']
    revoke_api_key(key_id, customer_id)
    messages.success(request, get_t(request)['msg_api_key_revoked'])
    return redirect('market:account_settings')


@role_required('customer')
def fleet_register(request):
    from .fleet import create_company, get_company_for_user
    from .emailer import notify_admin
    from .regions import get_or_create_region, get_region

    if _account_type(request) != 'b2b':
        raise Http404

    customer_id = request.session['picker_user']['id']
    t = get_t(request)

    if request.method == 'POST':
        if get_company_for_user(customer_id):
            messages.error(request, t['msg_fleet_already_registered'])
        else:
            region = get_or_create_region(request.POST.get('region_name', ''))
            company = create_company(
                customer_id,
                request.POST.get('company_name', ''),
                request.POST.get('registration_number', ''),
                region_id=region['id'] if region else None,
            )
            if company:
                messages.success(request, t['msg_fleet_registered'])
                notify_admin(
                    'New fleet registration awaiting approval',
                    f"{company['company_name']} (owner user_id {customer_id}) registered a delivery fleet and is awaiting approval.",
                )
            else:
                messages.error(request, t['msg_fleet_already_registered'])
        return redirect('market:fleet_register')

    company = get_company_for_user(customer_id)
    if company and company.get('region_id'):
        region = get_region(company['region_id'])
        company['region_name'] = region['name_en'] if region else ''
    profile_resp = get_client().table('pickker_customer_profiles').select('tin_number, business_license_number').eq('user_id', customer_id).execute()
    profile = profile_resp.data[0] if profile_resp.data else {}
    return render(request, 'market/fleet_register.html', {
        'company': company,
        'pending_message': t['fleet_pending_body'].format(company=company['company_name']) if company and not company['is_approved'] else '',
        'tin_number': profile.get('tin_number'),
        'business_license_number': profile.get('business_license_number'),
    })


@role_required('customer')
@require_POST
def fleet_update_region(request):
    """A company's own home region is informational only — shown to admin
    and on the company's own dashboard, never used to restrict matching
    (B2B stays interregional, as already decided this session)."""
    from .fleet import get_company_for_user, update_company_region
    from .regions import get_or_create_region

    customer_id = request.session['picker_user']['id']
    company = get_company_for_user(customer_id)
    if not company:
        raise Http404
    region = get_or_create_region(request.POST.get('region_name', ''))
    update_company_region(company['id'], region['id'] if region else None)
    messages.success(request, get_t(request)['msg_profile_updated'])
    return redirect('market:fleet_register')


def _owned_approved_company(request):
    """The fleet company owned by the logged-in customer — raises 404 if
    they don't own an approved one, so every fleet-management endpoint below
    is automatically scoped to the caller's own company."""
    from .fleet import get_company_for_user

    customer_id = request.session['picker_user']['id']
    company = get_company_for_user(customer_id)
    if not company or not company['is_approved']:
        raise Http404
    return company


@role_required('customer')
def fleet_dashboard(request):
    from .fleet import get_assignment_sla_badge, get_company_drivers, get_company_trucks, get_truck_warehouses, get_unassigned_orders_for_company, resolve_picker_truck_type
    from .fleet_earnings import get_company_earnings_by_driver, get_company_earnings_summary, get_company_orders
    from .fuel_logs import get_fuel_logs_for_company, get_fuel_summary_for_company
    from .picker_availability import get_order_counts_for_date_batch, get_picker_status
    from .signoff_forms import FIELD_CATALOG, get_submissions_for_company, get_templates_for_company
    from .truck_types import get_active_truck_types
    import datetime as _dt

    company = _owned_approved_company(request)
    trucks = get_company_trucks(company['id'])
    for truck in trucks:
        truck['warehouses'] = get_truck_warehouses(truck['id'])
    drivers = get_company_drivers(company['id'])

    today = _dt.date.today().isoformat()
    driver_ids = [d['user_id'] for d in drivers]
    order_counts = get_order_counts_for_date_batch(driver_ids, today) if driver_ids else {}
    for d in drivers:
        d['status_reason'] = get_picker_status(d['user_id']).get('reason')
        d['today_order_count'] = order_counts.get(d['user_id'], 0)

    # Orders that chose this company at checkout and are still waiting on
    # an internal driver decision — the company's own dispatch queue.
    # Truck types are looked up from the one already-fetched active list
    # (below), never re-queried per order/driver — this loop is otherwise
    # an N+1 waiting to happen against Supabase.
    truck_types_by_id = {tt['id']: tt for tt in get_active_truck_types(segment='b2b')}
    unassigned_orders = get_unassigned_orders_for_company(company['id'])
    for order in unassigned_orders:
        sla = get_assignment_sla_badge(order)
        order['sla_hours'], order['sla_class'] = sla if sla else (None, None)
        order_truck_type = truck_types_by_id.get(order.get('truck_type_id'))
        eligible_drivers = []
        for d in drivers:
            if not d.get('user', {}).get('is_active'):
                continue
            driver_truck_type = truck_types_by_id.get(resolve_picker_truck_type(d))
            if order_truck_type and driver_truck_type and float(driver_truck_type['max_weight_kg']) < float(order_truck_type.get('max_weight_kg') or 0):
                continue
            eligible_drivers.append(d)
        order['eligible_drivers'] = eligible_drivers
        order['has_delivering_driver'] = any(d.get('status_reason') == 'delivering' for d in eligible_drivers)
        # Flags whether picking from this list risks assigning a truck
        # that's already out on another delivery (possibly far from this
        # warehouse) — status_reason was already computed per-driver above
        # from get_picker_status, real-time (not date-scoped).
        order['has_delivering_driver'] = any(d.get('status_reason') == 'delivering' for d in eligible_drivers)

    from .stock import get_stock_for_vendor
    from .vendors import get_all_vendors, get_vendors_owned_by_company
    owned_vendors = get_vendors_owned_by_company(company['id'])
    for v in owned_vendors:
        v['stock'] = get_stock_for_vendor(v['vendor_type'], v['id'])
        v['tied_products'] = get_client().table('pickker_products').select('id, name, is_approved').eq('vendor_type', v['vendor_type']).eq('vendor_id', v['id']).execute().data

    # Every registered warehouse/industry, not just this company's own —
    # a company's trucks can service a warehouse they don't own (e.g. a
    # delivery agreement with another business), same as admin's own
    # truck-warehouse assignment page.
    warehouses = (
        [{**v, 'vendor_type': 'warehouse', 'vendor_key': f"warehouse:{v['id']}"} for v in get_all_vendors('warehouse')]
        + [{**v, 'vendor_type': 'industry', 'vendor_key': f"industry:{v['id']}"} for v in get_all_vendors('industry')]
    )

    from accounts.forms import ProductForm

    # Fuel log + sign-off tabs — decorated here (not in the template) with
    # driver name / truck nickname from the trucks/drivers already fetched
    # above, so the template never needs a dynamic dict lookup by id.
    trucks_by_id = {t['id']: t for t in trucks}
    drivers_by_id = {d['user_id']: d for d in drivers}

    def _driver_name(picker_id):
        d = drivers_by_id.get(picker_id)
        return f"{d['user']['first_name']} {d['user']['last_name']}".strip() if d else '—'

    def _truck_nickname(truck_id):
        t = trucks_by_id.get(truck_id)
        return t['nickname'] if t and t.get('nickname') else '—'

    fuel_logs = get_fuel_logs_for_company(company['id'])
    for f in fuel_logs:
        f['driver_name'] = _driver_name(f['picker_id'])
        f['truck_nickname'] = _truck_nickname(f.get('truck_id'))
    fuel_summary = get_fuel_summary_for_company(company['id'])
    for entry in fuel_summary['by_truck']:
        entry['truck_nickname'] = _truck_nickname(entry['truck_id'])

    t = get_t(request)
    field_catalog_for_ui = [
        {'key': k, 'type': v['type'], 'options': v.get('options'), 'default_label': t[v['default_label_key']]}
        for k, v in FIELD_CATALOG.items()
    ]
    signoff_templates = get_templates_for_company(company['id'])
    signoff_templates_by_id = {tpl['id']: tpl for tpl in signoff_templates}
    for tpl in signoff_templates:
        # Pre-resolved per-field checked/label state for the edit form —
        # Django templates can't do a dynamic dict lookup by loop variable,
        # so this is computed here instead of in the template.
        cfg = tpl.get('fields_config') or {}
        tpl['field_rows'] = [
            {
                'key': f['key'],
                'checked': bool(cfg.get(f['key'], {}).get('enabled')),
                'label': cfg.get(f['key'], {}).get('label') or f['default_label'],
            }
            for f in field_catalog_for_ui
        ]
    signoff_submissions = get_submissions_for_company(company['id'])
    for s in signoff_submissions:
        s['driver_name'] = _driver_name(s['picker_id'])
        tpl = signoff_templates_by_id.get(s['template_id'])
        s['template_name'] = tpl['name'] if tpl else '—'

    return render(request, 'market/fleet_dashboard.html', {
        'company': company,
        'trucks': trucks,
        'drivers': drivers,
        'truck_types': get_active_truck_types(segment='b2b'),
        'warehouses': warehouses,
        'center': DAR_ES_SALAAM_CENTER,
        'earnings_summary': get_company_earnings_summary(driver_ids),
        'earnings_by_driver': get_company_earnings_by_driver(driver_ids),
        'recent_orders': get_company_orders(driver_ids),
        'owned_vendors': owned_vendors,
        'unassigned_orders': unassigned_orders,
        'product_form': ProductForm(),
        'fuel_logs': fuel_logs,
        'fuel_summary': fuel_summary,
        'field_catalog_for_ui': field_catalog_for_ui,
        'signoff_templates': signoff_templates,
        'signoff_submissions': signoff_submissions,
    })


@role_required('customer')
def fleet_ai_insights(request):
    """On-demand (not loaded automatically with the dashboard) short AI
    summary of the company's own trucks/drivers/earnings/orders — a simple
    reuse of the same call_llm() the customer chat assistant uses."""
    from .fleet import get_company_drivers, get_company_trucks
    from .fleet_ai import get_company_insights
    from .fleet_earnings import get_company_earnings_summary, get_company_orders

    company = _owned_approved_company(request)
    trucks = get_company_trucks(company['id'])
    drivers = get_company_drivers(company['id'])
    driver_ids = [d['user_id'] for d in drivers]
    lang = request.session.get('language', 'en')
    insights = get_company_insights(
        company, trucks, drivers,
        get_company_earnings_summary(driver_ids),
        get_company_orders(driver_ids),
        lang=lang,
        force_refresh=request.GET.get('refresh') == '1',
    )
    return JsonResponse({'insights': insights})


@role_required('customer')
@require_POST
def fleet_register_vendor(request):
    """A company registering its own Warehouse or Industry — same
    create_vendor helper admin's page uses, just pre-scoped to the caller's
    own company_id so it's automatically theirs, and reachable without
    admin having to do it on their behalf first. Product-to-vendor tying
    stays admin-only for now (catalog integrity) — this only creates the
    vendor record itself."""
    from .vendors import create_vendor

    company = _owned_approved_company(request)
    vendor_type = request.POST.get('vendor_type')
    if vendor_type not in ('warehouse', 'industry'):
        raise Http404
    name = request.POST.get('name', '').strip()

    def _parse_coord(raw):
        raw = (raw or '').strip()
        return float(raw) if raw else None

    lat = _parse_coord(request.POST.get('lat'))
    lng = _parse_coord(request.POST.get('lng'))
    t = get_t(request)
    if name and lat is not None and lng is not None:
        create_vendor(
            vendor_type, request.POST.get('region', ''), name, request.POST.get('notes', ''), lat, lng,
            contact_phone=request.POST.get('contact_phone', ''),
            contact_person=request.POST.get('contact_person', ''),
            landmark_note=request.POST.get('landmark_note', ''),
            company_id=company['id'],
            storage_capacity_note=request.POST.get('storage_capacity_note', ''),
            license_number=request.POST.get('license_number', ''),
            bulk_only=request.POST.get('bulk_only') == 'on',
        )
        messages.success(request, t['msg_vendor_registered'].format(name=name))
    else:
        messages.error(request, t['msg_vendor_needs_location'])
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_receive_stock(request, vendor_type, vendor_id):
    """Same as admin's receive-stock action, but only for a vendor this
    company actually owns — checked explicitly since, unlike admin, a
    company's session has no universal access to every vendor."""
    from .stock import receive_stock
    from .vendors import get_vendor

    company = _owned_approved_company(request)
    vendor = get_vendor(vendor_type, vendor_id)
    if not vendor or vendor.get('company_id') != company['id']:
        raise Http404

    t = get_t(request)
    product_id = request.POST.get('product_id')
    quantity = request.POST.get('quantity', '').strip()
    try:
        product_id = int(product_id)
        quantity = int(quantity)
        if quantity <= 0:
            raise ValueError
    except (TypeError, ValueError):
        messages.error(request, t['msg_stock_invalid'])
        return redirect('market:fleet_dashboard')

    customer_id = request.session['picker_user']['id']
    receive_stock(vendor_type, vendor_id, product_id, quantity, customer_id)
    messages.success(request, t['msg_stock_received'].format(qty=quantity))
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_create_product(request, vendor_type, vendor_id):
    """A company creating its own product tied to its own warehouse/
    industry — reuses accounts.forms.ProductForm (and admin's own
    _build_product_row) so category/unit stay catalog-validated exactly
    like admin's product form, never free-typed. The vendor is taken from
    the URL (like fleet_receive_stock above), never a same-named POST
    field, and ownership + liveness are checked before anything else so a
    crafted request naming another company's vendor, or one admin has
    since deactivated, fails cleanly instead of surfacing as a confusing
    form-validation error. The new row always lands with is_approved=False
    — never visible/purchasable (see _visible() above) until an admin
    reviews it via the normal product_edit page."""
    from accounts.forms import ProductForm
    from accounts.views import _build_product_row, _save_product_image
    from .vendors import get_vendor

    company = _owned_approved_company(request)
    if vendor_type not in ('warehouse', 'industry'):
        raise Http404
    vendor = get_vendor(vendor_type, vendor_id)
    if not vendor or vendor.get('company_id') != company['id'] or not vendor.get('is_active'):
        raise Http404

    t = get_t(request)
    data = request.POST.copy()
    # The company never picks a vendor/market/stock figure themselves —
    # forced here so ProductForm validates as if they had, without ever
    # rendering those fields on the company-facing template. Real stock is
    # added afterward via the existing Receive Stock flow once approved.
    data['warehouse_id'] = str(vendor_id) if vendor_type == 'warehouse' else ''
    data['industry_id'] = str(vendor_id) if vendor_type == 'industry' else ''
    data['market_id'] = ''
    data['stock_qty'] = '0'
    data['is_trending'] = ''

    form = ProductForm(data, request.FILES)
    if not form.is_valid():
        messages.error(request, t['msg_product_submission_invalid'])
        return redirect('market:fleet_dashboard')

    row = _build_product_row(form)
    row['is_approved'] = False
    row['created_by'] = request.session['picker_user']['id']
    get_client().table('pickker_products').insert(row).execute()
    messages.success(request, t['msg_product_submitted'].format(name=row['name']))
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_request_payout(request):
    """A company can't trigger a payout directly (accounts:admin_pay_picker
    stays the only thing that flips pickker_picker_earnings to 'paid') — this
    just notifies admin that the company is asking, keeping a review step on
    a self-registered business's payout requests."""
    from .emailer import notify_admin
    from .fleet import get_company_driver_ids
    from .fleet_earnings import get_company_earnings_summary

    company = _owned_approved_company(request)
    driver_ids = get_company_driver_ids(company['id'])
    summary = get_company_earnings_summary(driver_ids)
    notify_admin(
        f"Payout requested: {company['company_name']}",
        f"{company['company_name']} (company_id {company['id']}) requested payout of its drivers' pending "
        f"earnings — TSh {summary['pending_amount']} pending across {summary['pending_count']} order(s).",
    )
    messages.success(request, get_t(request)['msg_payout_requested'])
    return redirect('market:fleet_dashboard')


@role_required('customer')
def fleet_message_send(request, driver_id):
    from .fleet_messaging import mark_fleet_messages_seen, send_fleet_message

    company = _owned_approved_company(request)
    driver_resp = get_client().table('pickker_picker_profiles').select('company_id').eq('user_id', driver_id).execute()
    if not driver_resp.data or driver_resp.data[0]['company_id'] != company['id']:
        raise Http404
    customer_id = request.session['picker_user']['id']
    message = send_fleet_message(company['id'], driver_id, customer_id, request.POST.get('message', ''))
    if message:
        mark_fleet_messages_seen(customer_id, company['id'], driver_id)
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        from .fleet_messaging import get_fleet_messages
        return JsonResponse({'messages': get_fleet_messages(company['id'], driver_id)})
    return redirect('market:fleet_dashboard')


@role_required('customer')
def fleet_message_poll(request, driver_id):
    from .fleet_messaging import get_fleet_messages, mark_fleet_messages_seen

    company = _owned_approved_company(request)
    driver_resp = get_client().table('pickker_picker_profiles').select('company_id').eq('user_id', driver_id).execute()
    if not driver_resp.data or driver_resp.data[0]['company_id'] != company['id']:
        raise Http404
    customer_id = request.session['picker_user']['id']
    mark_fleet_messages_seen(customer_id, company['id'], driver_id)
    return JsonResponse({'messages': get_fleet_messages(company['id'], driver_id)})


def get_fleet_live_positions(company):
    """One entry per driver of this company currently out on a delivery,
    each with a live position and a freshly recomputed real road route to
    that delivery's destination. Same live_tracking/geo primitives as
    accounts:admin_track_order_data, fanned out across every driver instead
    of one order. Plain function (no request/response) so both the fleet
    dashboard's poll view and the public API's fleet-trucks endpoint can
    call it without duplicating the query logic."""
    from .fleet import get_company_drivers
    from .geo import get_multi_stop_route
    from .live_tracking import get_live_location
    from .picker_availability import get_picker_status
    from .shop_payments import get_remaining_pickup_waypoints
    from .truck_types import get_truck_type

    drivers = get_company_drivers(company['id'])
    client = get_client()

    positions = []
    for d in drivers:
        status = get_picker_status(d['user_id'])
        if not status.get('delivering'):
            continue
        orders_resp = (
            client.table('pickker_orders')
            .select('id, status, delivery_lat, delivery_lng, picker_location_lat, picker_location_lng')
            .eq('picker_id', d['user_id']).in_('status', ['picking', 'in_transit']).limit(1).execute()
        )
        if not orders_resp.data:
            continue
        order = orders_resp.data[0]
        live = get_live_location(order['id'])
        lat = live['lat'] if live else order.get('picker_location_lat')
        lng = live['lng'] if live else order.get('picker_location_lng')
        if lat is None or lng is None or order.get('delivery_lat') is None:
            continue

        remaining_waypoints = get_remaining_pickup_waypoints(order['id'])
        route = get_multi_stop_route(float(lat), float(lng), remaining_waypoints, float(order['delivery_lat']), float(order['delivery_lng']))

        truck = get_truck_type(d.get('truck_type_id')) if d.get('truck_type_id') else None
        positions.append({
            'driver_id': d['user_id'],
            'driver_name': f"{d['user']['first_name']} {d['user']['last_name']}".strip(),
            'driver_phone': d['user'].get('phone_number'),
            'truck_nickname': None,
            'truck_type_name': truck['name'] if truck else None,
            'truck_icon': truck['icon'] if truck else None,
            'truck_icon_url': truck.get('icon_url') if truck else None,
            'order_id': order['id'],
            'order_status': order['status'],
            'lat': float(lat),
            'lng': float(lng),
            'route': {
                'distance_km': route['distance_km'],
                'duration_min': route['duration_min'],
                'geometry': route['geometry'],
            } if route else None,
        })

    trucks_resp = client.table('pickker_fleet_trucks').select('id, nickname').eq('company_id', company['id']).execute()
    trucks_by_driver_lookup = {t['id']: t['nickname'] for t in trucks_resp.data}
    drivers_by_id = {d['user_id']: d for d in drivers}
    for p in positions:
        driver = drivers_by_id.get(p['driver_id'])
        if driver and driver.get('truck_id') in trucks_by_driver_lookup:
            p['truck_nickname'] = trucks_by_driver_lookup[driver['truck_id']]

    return positions


@role_required('customer')
def fleet_live_positions(request):
    company = _owned_approved_company(request)
    return JsonResponse({'positions': get_fleet_live_positions(company)})


def _driver_active_order(company, driver_id):
    """The one order (if any) putting this driver of this company 'out on
    a delivery' right now — same lookup get_fleet_live_positions already
    does per-driver, factored out so the detail page and its poll endpoint
    both use the exact same definition of 'currently delivering'."""
    from .fleet import get_company_driver_ids

    if driver_id not in get_company_driver_ids(company['id']):
        return None
    resp = (
        get_client().table('pickker_orders').select('*')
        .eq('picker_id', driver_id).in_('status', ['picking', 'in_transit']).limit(1).execute()
    )
    return resp.data[0] if resp.data else None


@role_required('customer')
def fleet_track_driver(request, driver_id):
    """Company-scoped twin of accounts:admin_track_order — the detailed
    single-driver route/ETA page a fleet company gets for one of its own
    drivers, reusing the exact same live_tracking/geo helpers admin's page
    does. Purely additive: the existing multi-driver overview map/poll
    (fleet_live_positions above) is untouched by this."""
    from .geo import get_multi_stop_route
    from .live_tracking import get_live_location
    from .shop_payments import get_remaining_pickup_waypoints
    from .truck_types import get_picker_vehicle_display

    company = _owned_approved_company(request)
    order = _driver_active_order(company, driver_id)
    if not order or order.get('delivery_lat') is None:
        raise Http404

    client = get_client()
    driver_resp = client.table('pickker_users').select('first_name, last_name').eq('id', driver_id).execute()
    driver_name = None
    if driver_resp.data:
        du = driver_resp.data[0]
        driver_name = f"{du['first_name']} {du['last_name']}".strip()

    vehicle_display = get_picker_vehicle_display(driver_id)

    live = get_live_location(order['id'])
    start_lat = live['lat'] if live else order.get('picker_location_lat')
    start_lng = live['lng'] if live else order.get('picker_location_lng')
    route = None
    if start_lat is not None:
        remaining_waypoints = get_remaining_pickup_waypoints(order['id'])
        route = get_multi_stop_route(float(start_lat), float(start_lng), remaining_waypoints, float(order['delivery_lat']), float(order['delivery_lng']))

    picking_duration_min = None
    if order.get('picking_started_at') and order.get('delivery_started_at'):
        started = datetime.datetime.fromisoformat(order['picking_started_at'].replace('Z', '+00:00'))
        left = datetime.datetime.fromisoformat(order['delivery_started_at'].replace('Z', '+00:00'))
        picking_duration_min = round((left - started).total_seconds() / 60, 1)

    return render(request, 'market/fleet_track_driver.html', {
        'order': order,
        'driver_id': driver_id,
        'driver_name': driver_name,
        'picker_vehicle_type': vehicle_display['vehicle_type'],
        'picker_truck_icon': vehicle_display['truck_icon'],
        'picker_truck_icon_url': vehicle_display['truck_icon_url'],
        'start_lat': start_lat,
        'start_lng': start_lng,
        'route_distance_km': route['distance_km'] if route else order.get('distance_km'),
        'route_duration_min': route['duration_min'] if route else None,
        'route_geometry_json': json.dumps(route['geometry']) if route and route.get('geometry') else None,
        'picking_duration_min': picking_duration_min,
    })


@role_required('customer')
def fleet_track_driver_data(request, driver_id):
    """Polled from fleet_track_driver.html — company-scoped twin of
    accounts:admin_track_order_data."""
    from .geo import get_multi_stop_route
    from .live_tracking import get_live_location
    from .order_status import get_status_label
    from .shop_payments import get_remaining_pickup_waypoints

    company = _owned_approved_company(request)
    order = _driver_active_order(company, driver_id)
    if not order:
        return JsonResponse({'location': None, 'route': None, 'status': None, 'status_label': None, 'delivered': True})

    location = get_live_location(order['id'])
    route = None
    if location and order.get('delivery_lat') is not None:
        remaining_waypoints = get_remaining_pickup_waypoints(order['id'])
        route = get_multi_stop_route(location['lat'], location['lng'], remaining_waypoints, float(order['delivery_lat']), float(order['delivery_lng']))

    return JsonResponse({
        'location': location,
        'route': {
            'distance_km': route['distance_km'],
            'duration_min': route['duration_min'],
            'geometry': route['geometry'],
        } if route else None,
        'status': order['status'],
        'status_label': get_status_label(get_t(request), order['status']),
        'delivered': order['status'] == 'delivered',
    })


@role_required('customer')
@require_POST
def fleet_add_truck(request):
    from .fleet import add_truck, set_truck_warehouses

    company = _owned_approved_company(request)
    truck_type_id = request.POST.get('truck_type_id')
    if truck_type_id:
        truck = add_truck(company['id'], truck_type_id, request.POST.get('nickname', ''), request.POST.get('license_plate', ''))
        if truck:
            set_truck_warehouses(truck['id'], request.POST.getlist('warehouses'))
        messages.success(request, 'Truck added.')
    else:
        messages.error(request, 'Choose a truck type.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_edit_truck(request, truck_id):
    from .fleet import get_truck, set_truck_warehouses, update_truck

    company = _owned_approved_company(request)
    truck = get_truck(truck_id)
    if not truck or truck['company_id'] != company['id']:
        raise Http404
    truck_type_id = request.POST.get('truck_type_id')
    if truck_type_id:
        update_truck(truck_id, truck_type_id, request.POST.get('nickname', ''), request.POST.get('license_plate', ''))
        set_truck_warehouses(truck_id, request.POST.getlist('warehouses'))
        messages.success(request, 'Truck updated.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_toggle_truck(request, truck_id):
    from .fleet import deactivate_truck, get_truck, reactivate_truck

    company = _owned_approved_company(request)
    truck = get_truck(truck_id)
    if not truck or truck['company_id'] != company['id']:
        raise Http404
    if truck['is_active']:
        deactivate_truck(truck_id)
        messages.success(request, 'Truck deactivated.')
    else:
        reactivate_truck(truck_id)
        messages.success(request, 'Truck activated.')
    return redirect('market:fleet_dashboard')


def _signoff_fields_config_from_post(request):
    """Shared by create/update — reads the enabled-checkbox + label text
    input per catalog field key (fixed set, see market/signoff_forms.py's
    FIELD_CATALOG) posted from the fleet dashboard's template editor."""
    from .signoff_forms import FIELD_CATALOG

    config = {}
    for key in FIELD_CATALOG:
        enabled = request.POST.get(f'enabled_{key}') == 'on'
        label = request.POST.get(f'label_{key}', '').strip()
        if enabled or label:
            config[key] = {'enabled': enabled, 'label': label}
    return config


@role_required('customer')
@require_POST
def fleet_create_signoff_template(request):
    from .signoff_forms import create_template

    company = _owned_approved_company(request)
    name = request.POST.get('name', '').strip()
    if not name:
        messages.error(request, 'Give the template a name.')
        return redirect('market:fleet_dashboard')
    create_template(company['id'], name, _signoff_fields_config_from_post(request))
    messages.success(request, 'Template created.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_update_signoff_template(request, template_id):
    from .signoff_forms import get_template, update_template

    company = _owned_approved_company(request)
    template = get_template(template_id)
    if not template or template['company_id'] != company['id']:
        raise Http404
    name = request.POST.get('name', '').strip() or template['name']
    update_template(template_id, name, _signoff_fields_config_from_post(request))
    messages.success(request, 'Template updated.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_toggle_signoff_template(request, template_id):
    from .signoff_forms import get_template, set_template_active

    company = _owned_approved_company(request)
    template = get_template(template_id)
    if not template or template['company_id'] != company['id']:
        raise Http404
    set_template_active(template_id, not template['is_active'])
    messages.success(request, 'Template deactivated.' if template['is_active'] else 'Template activated.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_invite_driver(request):
    from django.urls import reverse

    from .emailer import send_email
    from .fleet import get_truck, invite_company_driver

    company = _owned_approved_company(request)
    truck_id = request.POST.get('truck_id')
    truck = get_truck(truck_id) if truck_id else None
    if not truck or truck['company_id'] != company['id']:
        messages.error(request, 'Choose one of your own trucks for this driver.')
        return redirect('market:fleet_dashboard')

    user, result = invite_company_driver(
        company['id'],
        request.POST.get('email', ''),
        request.POST.get('first_name', ''),
        request.POST.get('last_name', ''),
        request.POST.get('phone_number', ''),
        truck_id,
    )
    if not user:
        messages.error(request, result)
    else:
        set_password_url = absolute_url(request, reverse('accounts:reset_password', args=[result]))
        send_email(
            user['email'],
            f"You've been invited to drive for {company['company_name']} on PickkerMarket",
            f"Hi {user['first_name']},\n\n"
            f"{company['company_name']} has registered you as one of their delivery drivers on PickkerMarket.\n\n"
            f"Set your password to log in (this link is valid for 1 hour — if it expires, use "
            "\"Forgot password\" on the login page instead):\n"
            f"{set_password_url}\n\n"
            "— The PickkerMarket Team",
        )
        messages.success(request, f"{user['first_name']} invited — they'll set their password by email.")
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_reassign_driver(request, driver_id):
    from .fleet import get_truck, reassign_driver_truck

    company = _owned_approved_company(request)
    truck_id = request.POST.get('truck_id')
    truck = get_truck(truck_id) if truck_id else None
    if not truck or truck['company_id'] != company['id']:
        messages.error(request, 'Choose one of your own trucks.')
        return redirect('market:fleet_dashboard')

    driver_resp = get_client().table('pickker_picker_profiles').select('company_id').eq('user_id', driver_id).execute()
    if not driver_resp.data or driver_resp.data[0]['company_id'] != company['id']:
        raise Http404

    reassign_driver_truck(driver_id, truck_id)
    messages.success(request, 'Driver reassigned.')
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def fleet_assign_driver(request, order_id):
    """Company self-service assignment of one of its own drivers to an
    order that landed with the company at checkout (picker_id still None,
    company_id set — see market/views.py::checkout_provider). Thin wrapper
    around market/fleet.py::assign_driver_to_order, which does all the
    real validation (order ownership, driver belongs to company, driver's
    truck is weight-capable, driver has a known location)."""
    from .fleet import assign_driver_to_order

    company = _owned_approved_company(request)
    driver_id = request.POST.get('driver_id')
    t = get_t(request)
    if not driver_id:
        messages.error(request, t['fleet_no_eligible_drivers'])
        return redirect('market:fleet_dashboard')

    ok, reason = assign_driver_to_order(order_id, int(driver_id), company_id=company['id'])
    if ok:
        messages.success(request, t['msg_driver_assigned'])
    else:
        messages.error(request, t.get(f'msg_assign_{reason}', t['msg_assign_not_found']))
    return redirect('market:fleet_dashboard')


@role_required('customer')
@require_POST
def delete_account(request):
    customer_id = request.session['picker_user']['id']
    deleted, message = delete_or_suspend_self(customer_id, get_t(request))
    request.session.flush()
    messages.success(request, message)
    return redirect('market:home')


@role_required('customer')
def cook_assistant(request):
    result = None
    dish_query = ''
    error = None

    if request.method == 'POST' and 'dish' in request.POST:
        dish_query = request.POST.get('dish', '').strip()
        if dish_query:
            ai_result = get_ingredients_for_dish(dish_query)
            if ai_result and ai_result.get('ingredients'):
                products_resp = _visible(get_client().table('pickker_products').select('*')).execute()
                matched, unmatched = match_ingredients_to_products(ai_result['ingredients'], products_resp.data)
                unit_label_map = get_unit_label_map(request.session.get('language', 'en'))
                for entry in matched:
                    p = entry.get('product') if isinstance(entry, dict) else None
                    if p:
                        p['unit_display'] = unit_label_map.get(p['unit'], p['unit'])
                result = {
                    'dish': ai_result.get('dish', dish_query),
                    'matched': matched,
                    'unmatched': unmatched,
                }
            else:
                error = "Sorry, our assistant couldn't work that out right now. Please try again in a moment."

    return render(request, 'market/cook_assistant.html', {
        'dish_query': dish_query,
        'result': result,
        'error': error,
    })


@role_required('customer')
def cook_assistant_add_to_cart(request):
    if request.method == 'POST':
        customer_id = request.session['picker_user']['id']
        product_ids = [int(pid) for pid in request.POST.getlist('product_id')]
        # Unlike add_to_cart above, this loops straight from POSTed ids with
        # no lookup at all in the pre-existing code — a batch visibility
        # check here is the only thing standing between a stale/pending
        # product id and it silently landing in the cart.
        visible_ids = set()
        if product_ids:
            visible_resp = _visible(get_client().table('pickker_products').select('id')).in_('id', product_ids).execute()
            visible_ids = {p['id'] for p in visible_resp.data}
        added = 0
        for pid in product_ids:
            if pid in visible_ids:
                cart_utils.add_to_cart(customer_id, pid, 1)
                added += 1
        if added:
            messages.success(request, get_t(request)['msg_cook_added_to_cart'].format(count=added))
        return redirect('market:cart')
    return redirect('market:cook_assistant')


@role_required('customer')
@require_POST
def chat_message(request):
    try:
        payload = json.loads(request.body or '{}')
    except json.JSONDecodeError:
        payload = {}
    user_message = (payload.get('message') or '').strip()
    if not user_message:
        return JsonResponse({'error': 'Empty message'}, status=400)

    mode = payload.get('mode') or request.session.get('chat_mode') or DEFAULT_MODE
    if mode not in CHAT_MODES:
        mode = DEFAULT_MODE
    request.session['chat_mode'] = mode

    customer_id = request.session['picker_user']['id']

    try:
        # Load history: prefer session (fast), fall back to Supabase (persistent)
        history = request.session.get('chat_history')
        if history is None:
            history = load_chat_history(customer_id)
            request.session['chat_history'] = history

        # Load persistent memory notes
        memory_notes = load_chat_memory(customer_id)

        client = get_client()
        products_resp = _visible(client.table('pickker_products').select('*')).execute()
        products = products_resp.data
        products_by_id = {p['id']: p for p in products}

        cart = cart_utils.get_cart(customer_id)
        cart_items = []
        for row in cart:
            product = products_by_id.get(row['product_id'])
            if product:
                cart_items.append({'id': product['id'], 'name': product['name'], 'quantity': row['quantity']})

        account_type = _account_type(request)
        saved_location = get_customer_saved_location(customer_id) if mode == 'purchase' else None
        named_locations = get_named_locations(customer_id) if mode == 'purchase' and account_type != 'b2b' else None
        available_dates = describe_available_delivery_days() if mode == 'purchase' else None
        reply_text, action, quick_replies, new_memory_notes, do_clear_memory = get_chat_reply(
            history, user_message, products, cart_items, memory_notes, account_type, mode, saved_location, available_dates, named_locations,
            preferred_lang=request.session.get('language', 'en'),
        )

        added = []
        saved_plan_items = []
        cart_snapshot = None
        order_confirmation = None
        schedule_error = None
        schedule_error_detail = None
        if mode == 'purchase':
            if action and action.get('action') == 'schedule_order':
                result = schedule_order_from_chat(customer_id, account_type, action.get('delivery_date'), action.get('location_choice'))
                if 'error' in result:
                    schedule_error = result['error']
                    if schedule_error == 'named_location_not_set':
                        schedule_error = f"named_location_not_set_{result['location_choice']}"
                    elif schedule_error == 'out_of_stock':
                        schedule_error_detail = result.get('product_name')
                else:
                    order_confirmation = result
            else:
                added = apply_cart_action(action, products_by_id, customer_id, account_type)
                if added:
                    items, subtotal = _cart_items_and_subtotal(customer_id, account_type, lang=request.session.get('language', 'en'))
                    cart_snapshot = {
                        'items': [
                            {
                                'name': item['product'].get('display_name') or item['product']['name'],
                                'qty_unit_display': item['qty_unit_display'],
                                'line_total': float(item['line_total']),
                            }
                            for item in items
                        ],
                        'subtotal': float(subtotal),
                    }
        elif mode == 'plan':
            saved_plan_items = apply_save_plan_action(action, products_by_id, customer_id, account_type)

        # Handle memory actions from the AI
        if do_clear_memory:
            clear_chat_memory(customer_id)
            memory_notes = []
        elif new_memory_notes:
            # Merge new notes with existing, avoiding duplicates
            existing_lower = {n.lower() for n in memory_notes}
            for note in new_memory_notes:
                if note.lower() not in existing_lower:
                    memory_notes.append(note)
                    existing_lower.add(note.lower())
            save_chat_memory(customer_id, memory_notes)

        # Append to history and persist
        history.append({'role': 'user', 'content': user_message})
        history.append({'role': 'assistant', 'content': reply_text})
        trimmed = history[-40:]
        request.session['chat_history'] = trimmed
        request.session.modified = True
        save_chat_history(customer_id, trimmed)

        cart_count = sum(row['quantity'] for row in cart_utils.get_cart(customer_id))
        return JsonResponse({
            'reply': reply_text,
            'added_items': added,
            'saved_plan_items': saved_plan_items,
            'quick_replies': quick_replies,
            'cart_count': cart_count,
            'show_checkout_cta': cart_count > 0 and mode == 'purchase' and not order_confirmation,
            'memory_cleared': do_clear_memory,
            'cart_snapshot': cart_snapshot,
            'order_confirmation': order_confirmation,
            'schedule_error': schedule_error,
            'schedule_error_detail': schedule_error_detail,
        })
    except Exception:
        # Any unexpected failure here (e.g. a transient Supabase network error)
        # used to crash to Django's HTML error page, which broke the client's
        # response.json() parsing and surfaced as "couldn't reach the
        # assistant" — always return valid JSON instead, with a message the
        # customer can actually act on. The LLM call itself already has its
        # own multi-provider fallback chain (see llm_client.call_llm) and
        # only reaches this except block for failures outside that chain.
        logger.exception('chat_message failed unexpectedly (mode=%s)', mode)
        return JsonResponse({
            'reply': (
                "Samahani, kuna tatizo la muunganisho kwa sasa — tafadhali jaribu tena baada ya muda "
                "(Sorry, something went wrong on our end — please try again in a moment)."
            ),
            'added_items': [],
            'saved_plan_items': [],
            'quick_replies': [],
            'cart_count': 0,
            'show_checkout_cta': False,
            'memory_cleared': False,
            'cart_snapshot': None,
            'order_confirmation': None,
            'schedule_error': None,
            'schedule_error_detail': None,
        }, status=200)


@role_required('customer')
@require_POST
def chat_reset(request):
    """Clears chat history but keeps memory (preferences). Also clears the
    selected mode, so the mode picker is shown again for the next conversation."""
    customer_id = request.session['picker_user']['id']
    request.session['chat_history'] = []
    request.session.pop('chat_mode', None)
    request.session.modified = True
    clear_chat_history(customer_id)
    return JsonResponse({'ok': True, 'memory_kept': True})


@role_required('customer')
@require_POST
def chat_clear_memory(request):
    """Clears the persistent memory/preferences only."""
    customer_id = request.session['picker_user']['id']
    clear_chat_memory(customer_id)
    return JsonResponse({'ok': True})


@role_required('customer')
def chat_history_load(request):
    """Returns saved chat history for the frontend to restore on panel open."""
    customer_id = request.session['picker_user']['id']
    history = request.session.get('chat_history')
    if history is None:
        history = load_chat_history(customer_id)
        request.session['chat_history'] = history
        request.session.modified = True
    return JsonResponse({'history': history, 'mode': request.session.get('chat_mode')})


def fuzzy_area_search(request):
    """Public endpoint — no login required. Falls back to our local Dar es
    Salaam area database (pickker_known_areas), and then to the AI, when
    Nominatim returns nothing.

    Any coordinate the AI resolves here is written back to the known-areas
    table (see market/known_areas.py:save_area_coords) — either filling in
    an existing area that had no coordinates yet, or adding a brand new one
    under the AI's corrected name. That's what makes a customer's search
    permanently teach the location database instead of re-asking the AI for
    the same place every time."""
    from .known_areas import save_area_coords

    query = request.GET.get('q', '').strip()
    if not query:
        return JsonResponse({'match': None})

    result = fuzzy_match_area(query)

    if result and result['lat'] is not None:
        # We have a direct match with known coordinates
        return JsonResponse({
            'match': {
                'name': result['name'],
                'lat': result['lat'],
                'lng': result['lng'],
                'source': 'list'
            }
        })

    # AI Fallback: if we matched a name but have no coordinates yet, or if we
    # found no match at all, ask the AI for coordinates.
    search_name = result['name'] if result else query
    prompt = (
        f"A user in Tanzania is looking for a delivery location: '{search_name}'. "
        "Provide the approximate latitude and longitude for this neighborhood or landmark in Tanzania. "
        "Respond with ONLY valid JSON, no other text: "
        "{{\"name\": \"Corrected Name\", \"lat\": -6.xxx, \"lng\": 39.xxx}}. "
        "If you truly cannot identify the place in Tanzania, respond with {}."
    )
    raw_reply = call_llm([{'role': 'user', 'content': prompt}], temperature=0.1)
    if raw_reply:
        try:
            cleaned = raw_reply.strip()
            if cleaned.startswith('```'):
                match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', cleaned, re.DOTALL)
                if match:
                    cleaned = match.group(1)
                else:
                    cleaned = cleaned.strip('`').removeprefix('json').strip()

            data = json.loads(cleaned)
            if data and 'lat' in data and 'lng' in data and 'name' in data:
                lat, lng = float(data['lat']), float(data['lng'])
                # Only persist coordinates that plausibly land in the actual
                # service area — an AI guess outside it is more likely a
                # hallucination than a real new suburb, and shouldn't
                # pollute the gazetteer other customers' searches match against.
                if is_within_service_area(lat, lng):
                    save_area_coords(data['name'], lat, lng, source='ai')
                return JsonResponse({
                    'match': {
                        'name': data['name'],
                        'lat': lat,
                        'lng': lng,
                        'source': 'ai'
                    }
                })
        except Exception:
            pass

    return JsonResponse({'match': None})


def location_search(request):
    """Public endpoint — no login required. Every location search box on
    the site (signup, profile edit, checkout, admin edit) calls this
    instead of hitting Nominatim directly from the browser, so the search
    automatically benefits from geo.geocode()'s HERE fallback tier (better
    real-world address coverage than Nominatim alone) without any API key
    ever reaching client-side code. Cached briefly since the same handful
    of place names (markets, malls, landmarks) get typed by many different
    customers/pickers — a repeat search shouldn't cost a fresh Nominatim/
    HERE round-trip every time."""
    query = request.GET.get('q', '').strip().lower()
    if not query:
        return JsonResponse({'results': []})

    cache_key = f'location_search_{query}'
    cached = cache.get(cache_key)
    if cached is not None:
        return JsonResponse({'results': cached})

    results = geocode(query)
    cache.set(cache_key, results, 300)
    return JsonResponse({'results': results})


def region_search(request):
    """Public endpoint — no login required. Backs every region field in
    the app (fleet registration, picker profiles, known areas, the admin
    region catalog itself) — the caller types an address/area, HERE
    resolves it (deliberately HERE specifically, not the Nominatim-first
    geocode() used by location_search() above, since only HERE's response
    reliably carries a region/state field for Tanzania addresses in this
    codebase), and the frontend shows the distinct regions found for the
    user to pick from. Nothing is written yet here — market.regions
    ::get_or_create_region() only runs once a form is actually submitted
    with a chosen region_name, so an abandoned search never creates a
    stray region row."""
    from .here_api import here_geocode

    query = request.GET.get('q', '').strip()
    if not query:
        return JsonResponse({'results': []})

    cache_key = f'region_search_{query.lower()}'
    cached = cache.get(cache_key)
    if cached is not None:
        return JsonResponse({'results': cached})

    matches = here_geocode(query, limit=8)
    seen = set()
    results = []
    for m in matches:
        region_name = (m.get('region') or '').strip()
        key = region_name.lower()
        if not region_name or key in seen:
            continue
        seen.add(key)
        results.append({'label': m['label'], 'region_name': region_name, 'lat': m['lat'], 'lng': m['lng']})

    cache.set(cache_key, results, 300)
    return JsonResponse({'results': results})


def reverse_geocode_view(request):
    """Public endpoint — address label for a dropped/dragged map pin,
    wrapping geo.reverse_geocode() (Nominatim primary, HERE fallback) so
    the location pickers get a more reliable address without exposing any
    API key to the browser."""
    try:
        lat = float(request.GET.get('lat'))
        lng = float(request.GET.get('lng'))
    except (TypeError, ValueError):
        return JsonResponse({'address': None})
    return JsonResponse({'address': reverse_geocode(lat, lng)})


def reverse_geocode_components_view(request):
    """Same idea as reverse_geocode_view, but also splits out a best-guess
    city and neighbourhood/street name — used by the customer signup's "use
    my location" button to autofill separate City/Mtaa fields instead of
    just one combined address line."""
    try:
        lat = float(request.GET.get('lat'))
        lng = float(request.GET.get('lng'))
    except (TypeError, ValueError):
        return JsonResponse({'address': None, 'city': '', 'mtaa': '', 'region': ''})
    result = reverse_geocode_components(lat, lng)
    return JsonResponse(result or {'address': None, 'city': '', 'mtaa': '', 'region': ''})


def traffic_tile_proxy(request, z, x, y):
    """Serves HERE traffic-flow tiles same-origin so the API key never
    reaches the browser. Cached briefly — traffic changes but not
    instantly, and the same handful of tiles get re-requested by every
    picker/admin/customer looking at roughly the same city area. Returns
    204 (no content) rather than an error when HERE isn't configured or a
    tile isn't available, so the map just shows its plain OSM base with no
    traffic overlay instead of a broken-image icon."""
    from .here_api import here_traffic_tile

    cache_key = f'traffic_tile_{z}_{x}_{y}'
    cached = cache.get(cache_key)
    if cached is not None:
        content, content_type = cached
        return HttpResponse(content, content_type=content_type)

    result = here_traffic_tile(z, x, y)
    if not result:
        return HttpResponse(status=204)
    content, content_type = result
    cache.set(cache_key, (content, content_type), 90)
    return HttpResponse(content, content_type=content_type)


def weather_data(request):
    """Public endpoint — current weather near a lat/lng, for the small
    weather badge shown on delivery/tracking maps (genuinely useful for a
    picker deciding how to pack produce, not just decoration). Location is
    rounded to ~1km so nearby requests share one cached lookup instead of
    each pixel-perfect map center triggering its own HERE call."""
    from .here_api import here_weather

    try:
        lat = round(float(request.GET.get('lat')), 2)
        lng = round(float(request.GET.get('lng')), 2)
    except (TypeError, ValueError):
        return JsonResponse({'weather': None})

    cache_key = f'weather_{lat}_{lng}'
    cached = cache.get(cache_key, 'MISS')
    if cached != 'MISS':
        return JsonResponse({'weather': cached})

    weather = here_weather(lat, lng)
    cache.set(cache_key, weather, 600)
    return JsonResponse({'weather': weather})


def nearby_amenities(request):
    """Public endpoint — no login required, and no user-specific data
    involved (just a proxy over OSM's public Overpass data). Fetched
    asynchronously by the checkout/navigation maps so amenity markers
    (petrol stations, places of worship, hotels, restaurants, schools,
    markets, supermarkets) appear once loaded rather than blocking the map."""
    from .amenities import get_nearby_amenities

    try:
        lat = float(request.GET.get('lat'))
        lng = float(request.GET.get('lng'))
    except (TypeError, ValueError):
        return JsonResponse({'amenities': []})

    return JsonResponse({'amenities': get_nearby_amenities(lat, lng)})


def search_amenities(request):
    """Public endpoint backing the map search box — points are deliberately
    not auto-plotted (see market/amenities.py), so this is the only way they
    ever appear: the customer/picker searches a name or category and only
    the matches show up, keeping the map itself uncluttered."""
    from .amenities import search_amenities as do_search

    query = request.GET.get('q', '')
    try:
        lat = float(request.GET.get('lat'))
        lng = float(request.GET.get('lng'))
    except (TypeError, ValueError):
        lat = lng = None

    return JsonResponse({'results': do_search(query, lat, lng)})


def service_worker(request):
    """Served at the site root (not /static/js/) so its scope covers the
    whole app, not just the static/js/ directory it physically lives in."""
    sw_path = settings.BASE_DIR / 'static' / 'js' / 'service-worker.js'
    with open(sw_path, 'r', encoding='utf-8') as f:
        content = f.read()
    response = HttpResponse(content, content_type='application/javascript')
    response['Service-Worker-Allowed'] = '/'
    return response


@require_POST
def push_subscribe(request):
    """Any logged-in role (customer or picker) can subscribe — this isn't
    role-specific, unlike most views here, so it checks the session directly
    rather than using role_required."""
    picker_user = request.session.get('picker_user')
    if not picker_user:
        return JsonResponse({'ok': False}, status=401)
    try:
        subscription = json.loads(request.body)
    except (TypeError, ValueError):
        return JsonResponse({'ok': False}, status=400)
    ok = save_subscription(picker_user['id'], subscription)
    return JsonResponse({'ok': ok})


@require_POST
def push_unsubscribe(request):
    from .push_notifications import remove_subscription

    try:
        endpoint = json.loads(request.body).get('endpoint')
    except (TypeError, ValueError):
        endpoint = None
    if endpoint:
        remove_subscription(endpoint)
    return JsonResponse({'ok': True})
