import logging

from celery import shared_task
from django.conf import settings
from django.db import transaction
from django.utils import timezone

from shop.models import (
    ShippingCost,
    SKU,
    SKUPrice,
    SKUSynchronisationAttempt,
)
from shop.synchronisation.priorities import (
    CART,
    NEVER,
    PRIORITIES,
    VIEW,
    chunks,
    determine_sku_priority,
    eligible_sku_ids,
)
from shop.synchronisation.supplier import (
    get_supplier,
    is_temporary_supplier_error,
    validate_supplier_data,
)


logger = logging.getLogger(__name__)
QUEUE_BY_PRIORITY = {CART: "cart", VIEW: "view", NEVER: "never"}


def _dispatch(priority):
    queue = QUEUE_BY_PRIORITY[priority]
    count = 0
    sku_ids = eligible_sku_ids(priority).iterator(
        chunk_size=settings.SKU_SYNC_CHUNK_SIZE
    )
    for sku_id_chunk in chunks(sku_ids):
        process_sku_chunk.apply_async(
            args=(sku_id_chunk, priority),
            queue=queue,
        )
        count += 1
    return count


@shared_task(name="shop.synchronise_cart_skus")
def synchronise_cart_skus():
    return _dispatch(CART)


@shared_task(name="shop.synchronise_viewed_skus")
def synchronise_viewed_skus():
    return _dispatch(VIEW)


@shared_task(name="shop.synchronise_never_skus")
def synchronise_never_skus():
    return _dispatch(NEVER)


@shared_task(name="shop.process_sku_chunk")
def process_sku_chunk(sku_ids, source_priority, attempt_number=1):
    if source_priority not in PRIORITIES:
        raise ValueError(f"Unknown SKU priority: {source_priority}")
    if len(sku_ids) > settings.SKU_SYNC_CHUNK_SIZE:
        raise ValueError("INVALID_CHUNK_SIZE")

    skus = {
        sku.pk: sku
        for sku in SKU.objects.filter(pk__in=sku_ids)
        .select_related("productId", "productId__sellerId")
        .prefetch_related(
            "sku_skuprice__currencyId",
            "sku_skuprice__skuprice_shippingcost",
        )
    }
    suppliers = {}
    now = timezone.now()
    changed_skus = []
    changed_prices = []
    changed_shipping_costs = []
    attempts = []
    result = {"processed": 0, "skipped": 0, "failed": 0}

    for sku_id in dict.fromkeys(sku_ids):
        sku = skus.get(sku_id)
        if sku is None:
            continue

        if determine_sku_priority(sku) != source_priority:
            result["skipped"] += 1
            attempts.append(
                _attempt(sku, source_priority, "SKIPPED", attempt_number, now)
            )
            continue

        try:
            seller_id = sku.productId.sellerId_id
            if seller_id not in suppliers:
                suppliers[seller_id] = get_supplier(seller_id)
            supplier = suppliers[seller_id]
            supplier_data = validate_supplier_data(supplier.retrieve(sku))
            _apply_supplier_data(
                sku,
                supplier_data,
                now,
                changed_skus,
                changed_prices,
                changed_shipping_costs,
            )
        except Exception as error:  # one supplier failure must not stop a chunk
            result["failed"] += 1
            will_retry = (
                is_temporary_supplier_error(error)
                and attempt_number < settings.SKU_SYNC_MAX_ATTEMPTS
            )
            attempts.append(
                _attempt(
                    sku,
                    source_priority,
                    "RETRYING" if will_retry else "FAILED",
                    attempt_number,
                    now,
                    error,
                )
            )
            logger.warning(
                "SKU supplier synchronisation failed",
                exc_info=True,
                extra={"sku_id": sku.pk, "priority": source_priority},
            )
            if will_retry:
                process_sku_chunk.apply_async(
                    args=([sku.pk], source_priority, attempt_number + 1),
                    queue=QUEUE_BY_PRIORITY[source_priority],
                    countdown=settings.SKU_SYNC_RETRY_DELAY_SECONDS,
                )
            continue

        result["processed"] += 1
        attempts.append(
            _attempt(sku, source_priority, "SUCCEEDED", attempt_number, now)
        )

    with transaction.atomic():
        if changed_skus:
            SKU.objects.bulk_update(changed_skus, ("stock", "updatedDate"))
        if changed_prices:
            SKUPrice.objects.bulk_update(
                changed_prices,
                ("salePrice", "originalPrice", "updatedDate"),
            )
        if changed_shipping_costs:
            ShippingCost.objects.bulk_update(
                changed_shipping_costs,
                (
                    "isFreeShipping",
                    "minDeliveryDays",
                    "maxDeliveryDays",
                    "cost",
                    "defaultCost",
                    "updatedDate",
                ),
            )
        SKUSynchronisationAttempt.objects.bulk_create(attempts)
    return result


def _apply_supplier_data(
    sku,
    supplier_data,
    now,
    changed_skus,
    changed_prices,
    changed_shipping_costs,
):
    if "stock" in supplier_data and supplier_data["stock"] is not None:
        sku.stock = supplier_data["stock"]
        sku.updatedDate = now
        changed_skus.append(sku)

    prices_by_currency = {
        (price.currencyId.code or "").upper(): price
        for price in sku.sku_skuprice.all()
    }
    for supplier_price in supplier_data.get("prices", []):
        price = prices_by_currency.get(
            str(supplier_price.get("currency") or "").upper()
        )
        if price is None:
            continue
        if supplier_price.get("salePrice") is not None:
            price.salePrice = supplier_price["salePrice"]
        if supplier_price.get("originalPrice") is not None:
            price.originalPrice = supplier_price["originalPrice"]
        price.updatedDate = now
        changed_prices.append(price)

    for supplier_shipping in supplier_data.get("shippingOptions", []):
        price = prices_by_currency.get(
            str(supplier_shipping.get("currency") or "").upper()
        )
        if price is None:
            continue
        shipping = next(iter(price.skuprice_shippingcost.all()), None)
        if shipping is None:
            continue
        for field in (
            "isFreeShipping",
            "minDeliveryDays",
            "maxDeliveryDays",
            "cost",
            "defaultCost",
        ):
            if supplier_shipping.get(field) is not None:
                setattr(shipping, field, supplier_shipping[field])
        shipping.updatedDate = now
        changed_shipping_costs.append(shipping)


def _attempt(sku, priority, status, attempt_number, now, error=None):
    return SKUSynchronisationAttempt(
        skuId=sku,
        skuSerial=sku.serial,
        sourcePriority=priority,
        status=status,
        attemptNumber=attempt_number,
        errorMessage=str(error)[:2000] if error else None,
        createdDate=now,
    )
