"""Reconstruct a fitted ship from corptools asset rows — pure logic, no models of our
own. corptools containment is flat: an installed item is an asset whose
``location_id == ship.item_id``, tagged by ``location_flag``. The engine groups those
into fitting slots / drones / cargo / manifest, pairs charges to their module, and
denormalizes in-game type names. ``fit_to_eft`` serializes the fit-able parts to EFT
text (the ``manifest`` — SMB/holds — cannot be represented in EFT and is omitted)."""

from collections import defaultdict

from corptools.models import CorpAsset

from shop.sources import source_assets

SHIP_CATEGORY_ID = 6
CHARGE_CATEGORY_ID = 8

# location_flag prefix -> fitting-section key
_SLOT_PREFIXES = (
    ("HiSlot", "high"),
    ("MedSlot", "mid"),
    ("LoSlot", "low"),
    ("RigSlot", "rig"),
    ("SubSystemSlot", "subsystem"),
)
_DISPLAY_ORDER = ("high", "mid", "low", "rig", "subsystem")  # storefront table order
_EFT_ORDER = ("low", "mid", "high", "rig", "subsystem")  # EFT export order

_DRONE_FLAGS = {"DroneBay", "FighterBay"}


def slot_section(flag):
    """Fitting-section key for a location_flag (high/mid/low/rig/subsystem), or None
    when the flag is not a fitting slot."""
    for prefix, section in _SLOT_PREFIXES:
        if flag.startswith(prefix):
            return section
    return None


def _slot_index(flag):
    """Trailing slot number in a flag (``HiSlot3`` -> 3); 0 when absent."""
    digits = ""
    for ch in reversed(flag):
        if ch.isdigit():
            digits = ch + digits
        else:
            break
    return int(digits) if digits else 0


def _is_drone_flag(flag):
    return flag in _DRONE_FLAGS or flag.startswith("FighterTube")


def _categories_for(type_ids):
    """Map each type_id to its eve_sde category_id. Unknown ids are absent from the
    map. Resolved via eve_sde (complete, no ESI), like the other sections."""
    from eve_sde.models import ItemType

    rows = ItemType.objects.filter(id__in=list(type_ids)).values_list(
        "id", "group__category_id"
    )
    return {tid: cat for tid, cat in rows}


def fitted_ships_in_source(source, name_filter=""):
    """Assembled ship assets (singleton, ship category) sitting directly in a source's
    hangar/division. When ``name_filter`` is non-empty, keep only ships whose custom
    ``name`` contains it (case-insensitive). Returns a list of asset rows."""
    candidates = list(source_assets(source).filter(singleton=True))
    if not candidates:
        return []
    cats = _categories_for({a.type_id for a in candidates})
    ships = [a for a in candidates if cats.get(a.type_id) == SHIP_CATEGORY_ID]
    marker = name_filter.strip().lower()
    if marker:
        ships = [a for a in ships if a.name and marker in a.name.lower()]
    return ships


def _type_name(asset):
    """In-game type name for an asset row, falling back to the raw type_id string."""
    return asset.type_name.name if asset.type_name else str(asset.type_id)


def _installed_assets(ship_asset):
    """Direct children of a ship — assets whose ``location_id == ship.item_id``, owned
    by the same character/corporation. One level only (no recursion into SMB ships)."""
    model = type(ship_asset)
    qs = model.objects.filter(location_id=ship_asset.item_id).select_related(
        "type_name"
    )
    if isinstance(ship_asset, CorpAsset):
        return qs.filter(corporation_id=ship_asset.corporation_id)
    return qs.filter(character_id=ship_asset.character_id)


def reconstruct_fit(ship_asset):
    """Reconstruct a ship's fit from its installed assets into a JSON-able dict:
    ``{slots, drones, cargo, manifest}``. Names are denormalized (in-game type names).
    SMB/fleet-hangar/hold contents land in ``manifest`` and are NOT recursed."""
    children = list(_installed_assets(ship_asset))
    cats = _categories_for({c.type_id for c in children})

    slot_modules = {}  # flag -> module asset
    slot_charges = {}  # flag -> charge asset
    drones = defaultdict(lambda: [None, 0])  # type_id -> [name, qty]
    cargo = defaultdict(lambda: [None, 0])  # type_id -> [name, qty]
    manifest = defaultdict(lambda: [None, 0])  # (flag, type_id) -> [name, qty]

    # Invariant: a well-formed fit has at most one module and one charge per slot
    # flag, so a single dict slot is safe. Charge vs module is decided by eve_sde
    # category (charge = 8); this assumes the SDE is fully loaded (esde_load_sde) — an
    # incomplete SDE could mis-file an unknown-category charge as a module.
    for c in children:
        flag = c.location_flag
        if slot_section(flag) is not None:
            if cats.get(c.type_id) == CHARGE_CATEGORY_ID:
                slot_charges[flag] = c
            else:
                slot_modules[flag] = c
        elif _is_drone_flag(flag):
            entry = drones[c.type_id]
            entry[0], entry[1] = _type_name(c), entry[1] + c.quantity
        elif flag == "Cargo":
            entry = cargo[c.type_id]
            entry[0], entry[1] = _type_name(c), entry[1] + c.quantity
        else:
            entry = manifest[(flag, c.type_id)]
            entry[0], entry[1] = _type_name(c), entry[1] + c.quantity

    # a charge in a slot with no module (odd) falls back to cargo
    for flag, charge in slot_charges.items():
        if flag not in slot_modules:
            entry = cargo[charge.type_id]
            entry[0], entry[1] = _type_name(charge), entry[1] + charge.quantity

    slots = []
    for flag, module in slot_modules.items():
        charge = slot_charges.get(flag)
        slots.append(
            {
                "flag": flag,
                "type_id": module.type_id,
                "name": _type_name(module),
                "charge": (
                    {"type_id": charge.type_id, "name": _type_name(charge)}
                    if charge is not None
                    else None
                ),
            }
        )
    slots.sort(
        key=lambda s: (
            _DISPLAY_ORDER.index(slot_section(s["flag"])),
            _slot_index(s["flag"]),
        )
    )

    return {
        "slots": slots,
        "drones": sorted(
            ({"type_id": t, "name": n, "qty": q} for t, (n, q) in drones.items()),
            key=lambda d: d["name"],
        ),
        "cargo": sorted(
            ({"type_id": t, "name": n, "qty": q} for t, (n, q) in cargo.items()),
            key=lambda c: c["name"],
        ),
        "manifest": sorted(
            (
                {"flag": f, "type_id": t, "name": n, "qty": q}
                for (f, t), (n, q) in manifest.items()
            ),
            key=lambda m: (m["flag"], m["name"]),
        ),
    }


def fit_to_eft(hull_name, fit_name, fit):
    """Serialize a reconstructed fit (from ``reconstruct_fit``) to EFT text. Only the
    fit-able parts (slots, drones, cargo) are included; ``manifest`` is intentionally
    omitted — EFT cannot represent SMB/hold contents.

    Layout: ``[Hull, Name]`` header; each present fitting section (low, mid, high, rig,
    subsystem, in EFT order) as a block, blocks separated by a single blank line; then
    a double blank line before drones; a single blank line before cargo. A loaded
    charge is written ``Module, Charge``; drone/cargo quantities as ``xN``.

    ``fit_name`` is user-supplied (a ship's custom name); commas are stripped so they
    can't break the ``[Hull, Name]`` header on reimport."""
    fit_name = fit_name.replace(",", "")
    by_section = defaultdict(list)
    for s in fit.get("slots") or []:
        by_section[slot_section(s["flag"])].append(s)

    blocks = []
    for section in _EFT_ORDER:
        entries = sorted(
            by_section.get(section, []), key=lambda s: _slot_index(s["flag"])
        )
        if not entries:
            continue
        lines = []
        for s in entries:
            if s["charge"]:
                lines.append(f'{s["name"]}, {s["charge"]["name"]}')
            else:
                lines.append(s["name"])
        blocks.append("\n".join(lines))

    result = f"[{hull_name}, {fit_name}]"
    if blocks:
        result += "\n" + "\n\n".join(blocks)

    tail = []
    drones = fit.get("drones") or []
    cargo = fit.get("cargo") or []
    if drones:
        tail.append("\n".join(f'{d["name"]} x{d["qty"]}' for d in drones))
    if cargo:
        tail.append("\n".join(f'{c["name"]} x{c["qty"]}' for c in cargo))
    if tail:
        result += "\n\n\n" + "\n\n".join(tail)

    return result


_SECTION_LABELS = (
    ("high", "High"),
    ("mid", "Mid"),
    ("low", "Low"),
    ("rig", "Rigs"),
    ("subsystem", "Subsystems"),
)
_MANIFEST_LABELS = {
    "ShipHangar": "Ship Maintenance Bay",
    "FleetHangar": "Fleet Hangar",
    "SpecializedFuelBay": "Fuel Bay",
    "SpecializedOreHold": "Ore Hold",
    "SpecializedAmmoHold": "Ammo Hold",
    "SpecializedGasHold": "Gas Hold",
    "SpecializedMineralHold": "Mineral Hold",
    "SpecializedSalvageHold": "Salvage Hold",
}


def _manifest_label(flag):
    return _MANIFEST_LABELS.get(flag, flag)


def fit_display_groups(fit):
    """Presentation grouping of a reconstructed fit for the storefront detail table.

    Returns ``{fitting, drones, cargo, manifest}``: ``fitting`` is an ordered list of
    non-empty ``{label, count, items}`` (items sorted by slot index, each with its
    ``charge``); drones/cargo pass through; manifest items gain a human ``location``."""
    by_section = {}
    for s in fit.get("slots") or []:
        by_section.setdefault(slot_section(s["flag"]), []).append(s)
    fitting = []
    for key, label in _SECTION_LABELS:
        items = by_section.get(key)
        if items:
            items = sorted(items, key=lambda s: _slot_index(s["flag"]))
            fitting.append({"label": label, "count": len(items), "items": items})
    return {
        "fitting": fitting,
        "drones": list(fit.get("drones") or []),
        "cargo": list(fit.get("cargo") or []),
        "manifest": [
            {**m, "location": _manifest_label(m["flag"])}
            for m in fit.get("manifest") or []
        ],
    }
