"""Celery tasks: %-based Jita price refresh + the unified stock refresh.

Two fan-outs, both mirroring corptools's Beat-orchestrator pattern:
  * prices — one QueueOnce task per distinct %-priced hull type;
  * stock  — one beat (`refresh_all_stock`) that asks every registered
    SectionHandler to refresh its upstream source data, then recomputes stored
    stock per shop. Adding a section needs no new task or beat.
"""

import logging

import requests
from allianceauth.notifications import notify
from allianceauth.services.tasks import QueueOnce
from celery import shared_task
from django.conf import settings
from django.urls import reverse

from .market import jita_prices
from .models import PCT_MODES, HullListing, ModuleListing, Order

logger = logging.getLogger(__name__)

# Low priority so user-triggered work jumps ahead (AA priority: 1 urgent .. 9 idle).
TASK_PRIORITY = getattr(settings, "SHOP_TASKS_PRIORITY", 7)


def _order_summary(order):
    total = order.estimated_total_display
    if order.has_ask:
        total += " · some priced on request"
    return total


@shared_task
def notify_new_order(order_id):
    """Notify the shop owner of a new order: AA in-app always, Discord if set."""
    order = (
        Order.objects.select_related("shop", "buyer")
        .prefetch_related("lines__eve_type")
        .filter(pk=order_id)
        .first()
    )
    if order is None:
        return
    order_url = settings.SITE_URL + reverse(
        "backoffice:order_detail", args=[order.shop.pk, order.reference]
    )
    _notify_owner_in_app(order, order_url)
    _post_order_webhook(order, order_url)


def _notify_owner_in_app(order, order_url):
    count = len(order.lines.all())
    message = (
        f"{order.buyer.character_name} — {count} item(s), "
        f"{_order_summary(order)}\n{order_url}"
    )
    notify.info(order.shop.created_by, f"New order #{order.reference}", message)


def _post_order_webhook(order, order_url):
    if not order.shop.discord_webhook:
        return
    lines = "\n".join(
        f"{ln.quantity}× {ln.eve_type.name}"
        + (f" · {ln.label}" if ln.label else "")
        + ("" if ln.unit_price else " (Ask)")
        for ln in order.lines.all()
    )
    buyer_line = f"**Buyer:** {order.buyer.character_name}"
    if order.buyer.discord_handle:
        buyer_line += f"  ·  Discord: {order.buyer.discord_handle}"
    parts = [
        buyer_line,
        f"**Items:**\n{lines}",
        f"**Est. total:** {_order_summary(order)}",
    ]
    if order.additional_info:
        parts.append(f"**Note:** {order.additional_info}")
    embed = {
        "title": f"New order #{order.reference}",
        "url": order_url,
        "color": 0x635BFF,
        "description": "\n\n".join(parts),
        "footer": {"text": order.shop.name},
    }
    try:
        requests.post(order.shop.discord_webhook, json={"embeds": [embed]}, timeout=10)
    except Exception as exc:  # never let a notification break anything
        logger.warning("order webhook failed for #%s: %s", order.reference, exc)


# Listing models that carry a %-Jita price rule (same apply_jita_price contract).
# BPC prices are per-variant and set manually, so BPC is not on this path.
_PCT_PRICED_MODELS = (HullListing, ModuleListing)


def _apply_prices_for_type(type_id: int) -> int:
    """Recompute + store price for every %-listing of one type. Returns #changed."""
    prices = jita_prices(type_id)
    changed_total = 0
    for model in _PCT_PRICED_MODELS:
        changed = [
            listing
            for listing in model.objects.filter(
                eve_type_id=type_id, price_mode__in=PCT_MODES
            )
            if listing.apply_jita_price(prices["buy"], prices["sell"])
        ]
        if changed:
            model.objects.bulk_update(changed, ["price"])
        changed_total += len(changed)
    return changed_total


def pct_type_ids() -> list:
    """Distinct eve_type ids that appear on any %-priced listing (all priced models)."""
    ids = set()
    for model in _PCT_PRICED_MODELS:
        ids.update(
            model.objects.filter(price_mode__in=PCT_MODES).values_list(
                "eve_type_id", flat=True
            )
        )
    return list(ids)


def _enqueue_pct_refreshes() -> list:
    """Enqueue one per-type task for each distinct type on a %-listing."""
    type_ids = pct_type_ids()
    for type_id in type_ids:
        refresh_price_for_type.apply_async(args=[type_id], priority=TASK_PRIORITY)
    return type_ids


@shared_task(
    bind=True,
    base=QueueOnce,
    # timeout: lock TTL backstop so a crashed/killed task's per-type lock
    # self-heals in minutes instead of the 1h default (otherwise a worker crash
    # mid-task silently blocks that type's refresh until the default TTL).
    once={"keys": ["type_id"], "graceful": True, "timeout": 600},
    max_retries=3,
)
def refresh_price_for_type(self, type_id):
    try:
        _apply_prices_for_type(type_id)
    except Exception as exc:  # ESI hiccup -> retry with backoff, then keep-last-known
        raise self.retry(exc=exc, countdown=60)


@shared_task(base=QueueOnce)
def refresh_pct_prices():
    _enqueue_pct_refreshes()


def _refresh_shop_stock(shop_id):
    """Recompute one shop's stored stock across every registered section."""
    from .models import Shop
    from .sections.base import handlers

    shop = Shop.objects.filter(pk=shop_id).first()
    if shop is None:
        return
    for handler in handlers():
        handler.recompute(shop)


def _enqueue_stock_refreshes():
    """Enqueue one per-shop recompute for every shop any section flags as stale."""
    from .sections.base import handlers

    shop_ids = set()
    for handler in handlers():
        shop_ids.update(handler.shops_to_recompute())
    for shop_id in shop_ids:
        refresh_stock_for_shop.apply_async(args=[shop_id], priority=TASK_PRIORITY)
    return sorted(shop_ids)


@shared_task(
    bind=True,
    base=QueueOnce,
    once={"keys": ["shop_id"], "graceful": True, "timeout": 600},
    max_retries=3,
)
def refresh_stock_for_shop(self, shop_id):
    _refresh_shop_stock(shop_id)


@shared_task(
    base=QueueOnce,
    once={"keys": ["character_id"], "graceful": True, "timeout": 600},
)
def refresh_blueprints_for_character(character_id):
    """Refresh the ME/TE/runs blueprint mirror for one character (bpc source data).

    Recompute is left to the per-shop pass (`refresh_stock_for_shop`), which reads
    the freshened mirror — so blueprint stock stays consistent with every other
    section under the single refresh_all_stock beat.
    """
    from corptools.models import CharacterAudit

    from .sections.bpc.blueprints import refresh_character_blueprints

    audit = CharacterAudit.objects.filter(pk=character_id).first()
    if audit is not None:
        refresh_character_blueprints(audit)


@shared_task(
    base=QueueOnce,
    once={"keys": ["corporation_id"], "graceful": True, "timeout": 600},
)
def refresh_blueprints_for_corporation(corporation_id):
    """Refresh the ME/TE/runs blueprint mirror for one corporation (bpc source data).

    Recompute is left to the per-shop pass, which reads the freshened mirror — so
    blueprint stock stays consistent under the single refresh_all_stock beat.
    """
    from corptools.models import CorporationAudit

    from .sections.bpc.blueprints import refresh_corporation_blueprints

    audit = CorporationAudit.objects.filter(pk=corporation_id).first()
    if audit is not None:
        refresh_corporation_blueprints(audit)


@shared_task(base=QueueOnce)
def refresh_all_stock():
    """Single periodic entry point: refresh every section's upstream source data
    (corptools assets, ESI blueprints, ...), then recompute stored stock per shop.

    Source pulls are async, so a given cycle recomputes against the previous cycle's
    freshly-pulled data — eventual consistency, same as the corptools asset path.
    New sections plug in via their SectionHandler; no new task/beat is needed.
    """
    from .sections.base import handlers

    for handler in handlers():
        handler.enqueue_source_refresh()
    _enqueue_stock_refreshes()
