"""Fitted-ship recompute: reconcile a shop's FittedListing rows against the assembled
ships currently in its stockrooms — auto-list new ones, re-snapshot fits, drop gone
ones. One row per physical ship (item_id); stock is implicitly 1."""

import logging
import re

from django.db.models import F

from ...models import FITTED, FittedListing
from .reconstruct import fitted_ships_in_source, reconstruct_fit

logger = logging.getLogger(__name__)

# A listing's ship must be missing this many consecutive reconciles before it's removed,
# so a brief corptools asset-refresh window can't wipe listings + their manual prices.
MISS_LIMIT = 2


def _strip_marker(name, marker):
    """Buyer-facing ship name with EVERY occurrence of the curation marker removed
    (case-insensitive) and whitespace collapsed. Blank name -> empty string; blank
    marker -> just trimmed. The marker is normalized the same way detection normalizes
    it (``.strip()``) so no residue leaks to the public storefront."""
    if not name:
        return ""
    marker = marker.strip()
    if not marker:
        return name.strip()
    stripped = re.sub(re.escape(marker), "", name, flags=re.IGNORECASE)
    return " ".join(stripped.split())  # collapse the gaps the removed markers leave


def reconcile_fitted_listings(shop) -> None:
    """Upsert a FittedListing per assembled ship in the shop's fitted stockrooms and
    reconcile listings whose ship is gone: `persist` listings are kept as
    build-to-order (`stock=0`, last fit snapshot retained), the rest are deleted.
    Preserves `featured`/`price`/`persist`/`description` on existing rows (only the
    hull, name, fit snapshot and `stock` are refreshed for present ships)."""
    from eveuniverse.models import EveType

    seen = set()
    has_sources = shop.sources.filter(section=FITTED).exists()
    for source in shop.sources.filter(section=FITTED):
        for ship in fitted_ships_in_source(source, source.name_filter):
            # The ship is physically present — mark it seen BEFORE resolving its hull
            # type, so a transient ESI failure never drops an existing listing (which
            # would wipe its manual price / featured state). We just skip refreshing it
            # this cycle; a later successful pass re-snapshots it.
            seen.add(ship.item_id)
            try:
                EveType.objects.get_or_create_esi(id=ship.type_id)
            except Exception:  # noqa: BLE001 - skip unresolvable hull type
                logger.warning("skipping unresolvable hull type %s", ship.type_id)
                continue
            ship_name = (
                _strip_marker(ship.name, source.name_filter)
                if source.strip_marker
                else (ship.name or "").strip()
            )
            FittedListing.objects.update_or_create(
                shop=shop,
                item_id=ship.item_id,
                defaults={
                    "eve_type_id": ship.type_id,
                    "ship_name": ship_name,
                    "fit_json": reconstruct_fit(ship),
                    "stock": 1,
                    "miss_count": 0,  # present -> reset the grace counter
                },
            )

    if not has_sources:
        # No fitted stockrooms at all -> every listing is orphaned (e.g. the seller
        # removed the stockroom). Unambiguous, so clean up immediately.
        shop.fitted_listings.filter(persist=False).delete()
        shop.fitted_listings.filter(persist=True).update(stock=0)
        return

    # Sources exist but a ship wasn't seen: it may genuinely be gone, OR this may be a
    # corptools asset-refresh window (assets briefly deleted, corp division tagging not
    # yet restored, or character ship names not yet backfilled so a name filter matches
    # nothing). Require MISS_LIMIT consecutive misses before deleting, so a brief window
    # never wipes listings (and their manual prices); a truly-gone ship still clears in
    # a couple of cycles.
    gone = shop.fitted_listings.exclude(item_id__in=seen)
    gone.update(miss_count=F("miss_count") + 1)
    confirmed_gone = shop.fitted_listings.exclude(item_id__in=seen).filter(
        miss_count__gte=MISS_LIMIT
    )
    confirmed_gone.filter(persist=False).delete()
    confirmed_gone.filter(persist=True).update(stock=0)  # keep as build-to-order
