from decimal import ROUND_HALF_UP, Decimal

from .site_settings import get_fee_settings


def _apply_quick_surcharge(fee, is_quick, fee_settings):
    if not is_quick:
        return fee
    pct = Decimal(str(fee_settings['quick_delivery_fee_pct']))
    return fee * (Decimal('1') + pct / Decimal('100'))


def calculate_delivery_fee(distance_km, per_km_rate=None, is_quick=False):
    """B2C delivery fee = per-km rate * distance. The rate is normally the
    assigned picker's own vehicle type's admin-editable per-km rate (see
    market/truck_types.py — every vehicle type, B2C or B2B, has one),
    passed in by the caller; falls back to the historical flat
    `delivery_fee_per_km` when there's no picker yet or their vehicle type
    has no rate of its own set."""
    fee_settings = get_fee_settings()
    if per_km_rate is None:
        per_km_rate = fee_settings['delivery_fee_per_km']
    per_km_rate = Decimal(str(per_km_rate))
    fee = per_km_rate if distance_km is None else per_km_rate * Decimal(str(distance_km))
    fee = _apply_quick_surcharge(fee, is_quick, fee_settings)
    # Round to the nearest 100 TSh for a clean number
    return (fee / 100).quantize(Decimal('1'), rounding=ROUND_HALF_UP) * 100


def calculate_b2b_delivery_fee(distance_km, truck_type, per_km_rate=None, is_quick=False, total_weight_kg=None):
    """B2B delivery fee = the chosen truck type's flat base rate (already
    reflects its size/capacity) plus a B2B-specific per-km rate plus an
    explicit per-kg amount for the order's total weight (admin-editable,
    defaults to 0 — see b2b_delivery_fee_per_kg). The weight-tiered picking
    fee (calculate_picking_fee) still applies on top of this separately;
    this per-kg term ties weight directly into the transport leg itself,
    not just which truck tier is even eligible."""
    fee_settings = get_fee_settings()
    if per_km_rate is None:
        per_km_rate = fee_settings['b2b_delivery_fee_per_km']
    per_km_rate = Decimal(str(per_km_rate))
    per_kg_rate = Decimal(str(fee_settings['b2b_delivery_fee_per_kg']))
    base_rate = Decimal(str(truck_type['base_rate']))
    fee = base_rate if distance_km is None else base_rate + per_km_rate * Decimal(str(distance_km))
    if total_weight_kg is not None:
        fee += per_kg_rate * Decimal(str(total_weight_kg))
    fee = _apply_quick_surcharge(fee, is_quick, fee_settings)
    return (fee / 100).quantize(Decimal('1'), rounding=ROUND_HALF_UP) * 100


def calculate_picking_fee(total_weight_kg, items_by_category=None, fee_settings=None):
    """Weight-tiered base fee compensating the picker for gathering the
    order, plus an optional per-category "extra stops" surcharge on top.

    The base tiers alone treat 50kg of one item the same as 50kg spread
    across many different items — but in a real market, gathering many
    different items costs a picker real extra ground covered, and *how
    much* extra depends on the category: grains or fruits typically
    cluster in one area of the market (little to no extra roaming even
    with several different grains/fruits in the order), while vegetables
    are often scattered across many separate stalls (each additional
    distinct vegetable can mean walking to another stall entirely).

    `items_by_category` — {category_name: count_of_distinct_products_in_it}
    for this order — lets the fee reflect that: for each category, every
    item beyond the first adds `picking_extra_item_fee` (admin-set in Fee
    Settings, default 0 — opt-in) times that category's dispersion factor
    (admin-set per category, market/categories.py). A single item in any
    category, of any weight, is unaffected — the surcharge only applies to
    the 2nd+ distinct item within the same category."""
    if fee_settings is None:
        fee_settings = get_fee_settings()
    weight = Decimal(str(total_weight_kg or 0))
    if weight <= Decimal(str(fee_settings['picking_tier1_max_kg'])):
        fee = Decimal(str(fee_settings['picking_tier1_fee']))
    elif weight <= Decimal(str(fee_settings['picking_tier2_max_kg'])):
        fee = Decimal(str(fee_settings['picking_tier2_fee']))
    elif weight <= Decimal(str(fee_settings['picking_tier3_max_kg'])):
        fee = Decimal(str(fee_settings['picking_tier3_fee']))
    else:
        fee = Decimal(str(fee_settings['picking_tier4_fee']))

    extra_item_fee = Decimal(str(fee_settings.get('picking_extra_item_fee') or 0))
    if items_by_category and extra_item_fee > 0:
        from .categories import get_category_dispersion_map
        dispersion_map = get_category_dispersion_map()
        for category, distinct_count in items_by_category.items():
            extra_items = max(0, distinct_count - 1)
            if extra_items:
                dispersion = Decimal(str(dispersion_map.get(category, 1.0)))
                fee += extra_item_fee * extra_items * dispersion
    return fee


def calculate_package_fee(total_weight_kg, total_quantity, fee_settings=None):
    """Packaging fee (boxes/sacks/wrapping), tiered by size — separate from
    both the delivery fee (transport) and the picking fee (labor). An order
    lands in the largest tier that either its weight or its item quantity
    qualifies for, so a bulky-but-light order (many small items) still gets
    adequate packaging, not just a heavy one."""
    if fee_settings is None:
        fee_settings = get_fee_settings()
    weight = Decimal(str(total_weight_kg or 0))
    qty = int(total_quantity or 0)

    def tier_for(value, small_max, medium_max):
        if value <= small_max:
            return 0
        if value <= medium_max:
            return 1
        return 2

    weight_tier = tier_for(weight, Decimal(str(fee_settings['package_small_max_kg'])), Decimal(str(fee_settings['package_medium_max_kg'])))
    qty_tier = tier_for(qty, fee_settings['package_small_max_qty'], fee_settings['package_medium_max_qty'])
    tier = max(weight_tier, qty_tier)
    if tier == 0:
        return Decimal(str(fee_settings['package_fee_small']))
    if tier == 1:
        return Decimal(str(fee_settings['package_fee_medium']))
    return Decimal(str(fee_settings['package_fee_large']))


def apply_vendor_fee_waivers(vendor, total_weight_kg, delivery_fee, picking_fee):
    """A warehouse/industry can set an admin-editable weight threshold above
    which delivery and/or picking is free for that vendor — independent
    toggles, since a vendor might want to waive one but not the other.
    `vendor` may be None (a market-tied cart has no such threshold) — fails
    open, returns the fees unchanged."""
    if not vendor or total_weight_kg is None:
        return delivery_fee, picking_fee
    weight = Decimal(str(total_weight_kg))
    free_delivery_min_kg = vendor.get('free_delivery_min_kg')
    if free_delivery_min_kg is not None and weight >= Decimal(str(free_delivery_min_kg)):
        delivery_fee = Decimal('0')
    free_picking_min_kg = vendor.get('free_picking_min_kg')
    if free_picking_min_kg is not None and weight >= Decimal(str(free_picking_min_kg)):
        picking_fee = Decimal('0')
    return delivery_fee, picking_fee


def get_effective_price(product, account_type):
    """B2B buyers get the admin-set wholesale price if one exists, otherwise
    fall back to the normal market price."""
    if account_type == 'b2b' and product.get('b2b_price') is not None:
        return Decimal(str(product['b2b_price']))
    return Decimal(str(product['market_price']))


def get_min_qty(product, account_type):
    """Every product has an admin-editable general minimum order quantity
    (min_purchase_qty, defaults to 1 — i.e. no restriction). B2B buyers must
    additionally clear the product's wholesale minimum on top of that, since
    the two are independent admin knobs (a product can have a plain
    storefront minimum of 3 and a separate, usually higher, B2B minimum of
    10)."""
    general_min = int(product.get('min_purchase_qty') or 1)
    if account_type == 'b2b':
        return max(general_min, int(product.get('b2b_min_qty') or 10))
    return general_min
