import { Collection } from "./collection.js";
import { AbilityView, FactionView, UnitView, WeaponKeywordView, WeaponView, } from "./entities.js";
import { emptyRawData } from "./types.js";
import { RAW_DATA } from "./bundle.generated.js";
import { resolveLayout } from "../terrain/resolve.js";
import { resolveEligibleAbilities, } from "../abilities-resolver/index.js";
/** The whole dataset, with linked accessors over every entity collection. */
export class Dataset {
    // Richly-linked collections.
    units;
    weapons;
    weaponKeywords;
    factions;
    abilities;
    // Id-bearing collections without bespoke views (records returned as-is).
    detachments;
    enhancements;
    stratagems;
    wargearOptions;
    wargear;
    missions;
    missionMatchups;
    missionCards;
    deploymentPatterns;
    forceDispositions;
    terrainTemplates;
    terrainLayouts;
    resourcePools;
    // Id-less collections, exposed as plain arrays.
    leaderAttachments;
    unitCompositions;
    gameVersions;
    timingFlags;
    interactionFlags;
    phaseMappings;
    /** `source_type:source_id` → unioned phases. */
    phaseIndex = new Map();
    /** ability id → units that list it. */
    unitsByAbility = new Map();
    /** weapon id → units that list it. */
    unitsByWeapon = new Map();
    /** weapon-keyword id → weapons whose profiles reference it. */
    weaponsByKeyword = new Map();
    /** unit id → wargear options authored for it (declared order preserved). */
    wargearOptionsByUnit = new Map();
    constructor(raw = emptyRawData()) {
        this.units = new Collection({
            items: raw.units,
            idOf: (u) => u.id,
            // The same unit id is shared across factions (e.g. ministorum-priest);
            // keep each faction's copy, collapse only true within-faction duplicates.
            dedupeKeyOf: (u) => `${u.faction_id}::${u.id}`,
            nameOf: (u) => u.name,
            factionOf: (u) => u.faction_id,
            wrap: (u) => new UnitView(u, this),
        });
        this.weapons = new Collection({
            items: raw.weapons,
            idOf: (w) => w.id,
            nameOf: (w) => w.name,
            wrap: (w) => new WeaponView(w, this),
        });
        this.weaponKeywords = new Collection({
            items: raw.weaponKeywords,
            idOf: (k) => k.id,
            nameOf: (k) => k.name,
            wrap: (k) => new WeaponKeywordView(k, this),
        });
        this.factions = new Collection({
            items: raw.factions,
            idOf: (f) => f.id,
            nameOf: (f) => f.name,
            wrap: (f) => new FactionView(f, this),
        });
        this.abilities = new Collection({
            items: raw.abilities,
            idOf: (a) => a.ability_id,
            nameOf: (a) => a.name,
            factionOf: (a) => a.faction_id,
            wrap: (a) => new AbilityView(a, this),
        });
        this.detachments = idCollection(raw.detachments, (d) => d.faction_id);
        this.enhancements = idCollection(raw.enhancements);
        this.stratagems = idCollection(raw.stratagems);
        this.wargearOptions = idCollection(raw.wargearOptions);
        this.wargear = idCollection(raw.wargear);
        this.missions = idCollection(raw.missions);
        this.missionMatchups = idCollection(raw.missionMatchups);
        this.missionCards = idCollection(raw.missionCards);
        this.deploymentPatterns = idCollection(raw.deploymentPatterns);
        this.forceDispositions = idCollection(raw.forceDispositions);
        this.terrainTemplates = idCollection(raw.terrainTemplates);
        this.terrainLayouts = idCollection(raw.terrainLayouts);
        this.resourcePools = idCollection(raw.resourcePools);
        this.leaderAttachments = raw.leaderAttachments;
        this.unitCompositions = raw.unitCompositions;
        this.gameVersions = raw.gameVersions;
        this.timingFlags = raw.timingFlags;
        this.interactionFlags = raw.interactionFlags;
        this.phaseMappings = raw.phaseMappings;
        this.buildIndexes(raw);
    }
    /** The dataset built from the package's embedded data. */
    static embedded() {
        return new Dataset(RAW_DATA);
    }
    /** Phases a source acts in, unioned across its phase-mappings. */
    phasesFor(sourceType, sourceId) {
        return this.phaseIndex.get(`${sourceType}:${sourceId}`) ?? [];
    }
    /**
     * Resolve a terrain layout to absolute board-space vertices using this
     * dataset's embedded terrain-template catalog — the layout-id →
     * renderable-geometry hop. Mirror of Rust `Dataset::resolve_terrain`; the
     * geometry is pinned by the `terrain-resolver` conformance corpus.
     */
    resolveTerrain(layout) {
        // The resolver takes its own structurally-identical input types, decoupled
        // from the generated `anyOf`/newtype shapes; the underlying JSON is the same.
        return resolveLayout(layout, this.terrainTemplates.all);
    }
    /**
     * The terrain layouts a deployment pattern recommends, in declared order,
     * skipping any ids absent from the dataset.
     */
    recommendedTerrainLayouts(pattern) {
        return (pattern.recommended_terrain_layout_ids ?? [])
            .map((id) => this.terrainLayouts.get(id))
            .filter((l) => l !== undefined);
    }
    /** Units that list the given ability id. */
    unitsWithAbility(abilityId) {
        return (this.unitsByAbility.get(abilityId) ?? []).map((u) => new UnitView(u, this));
    }
    /** Units that list the given weapon id. */
    unitsWithWeapon(weaponId) {
        return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));
    }
    /** Weapons whose profiles reference the given weapon-keyword id. */
    weaponsWithKeyword(keywordId) {
        return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));
    }
    /**
     * Wargear options authored for the given unit, in declared order. Mirror of
     * Rust `Dataset::wargear_options_of`. Empty for a unit with no options.
     */
    wargearOptionsOf(unit) {
        return this.wargearOptionsByUnit.get(unit.id) ?? [];
    }
    /**
     * Leaders whose leader-attachment data lists `bodyguardUnitId` among its
     * eligible body units, sorted by name. The attachment is stored on the
     * leader pointing down to its bodyguards, so answering "which leaders can
     * attach to this unit?" means scanning the attachment list. Returns an empty
     * array for a unit that no leader can attach to (including leader units).
     */
    leadersAttachableTo(bodyguardUnitId) {
        return this.leaderAttachments
            .filter((la) => la.eligible_bodyguard_ids.includes(bodyguardUnitId))
            .map((la) => this.units.get(la.leader_id))
            .filter((u) => u !== undefined)
            .sort((a, b) => a.name.localeCompare(b.name));
    }
    /**
     * The inverse of {@link leadersAttachableTo}: the body units the given
     * leader can attach to, sorted by name. Scans the same leader-attachment
     * data from the leader's side (`leader_id` matches; resolve each
     * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader
     * unit. Together the two queries give the bidirectional attachment graph the
     * SPA needs to offer a partner dropdown from either end.
     */
    bodyguardsAttachableFrom(leaderUnitId) {
        const seen = new Set();
        const out = [];
        for (const la of this.leaderAttachments) {
            if (la.leader_id !== leaderUnitId)
                continue;
            for (const bodyguardId of la.eligible_bodyguard_ids) {
                if (seen.has(bodyguardId))
                    continue;
                const unit = this.units.get(bodyguardId);
                if (!unit)
                    continue;
                seen.add(bodyguardId);
                out.push(unit);
            }
        }
        return out.sort((a, b) => a.name.localeCompare(b.name));
    }
    /**
     * Enumerate every ability that could apply to the given unit in `phase`,
     * grouped by source. The SPA uses this to render the abilities pane.
     */
    eligibleAbilities(input, phase) {
        return resolveEligibleAbilities(this, input, phase);
    }
    /**
     * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:
     * intrinsic weapon-profile keywords plus every eligible ability whose DSL
     * effect translates to an attacker-side buff (army, detachment, unit,
     * attached members, support, plus any stratagems the caller has opted into).
     *
     * The result includes only buffs the buff layer can express today — the
     * `unsupported` half of the DSL→Buff translation is dropped here so callers
     * who just want the stack don't need to thread diagnostics through. Use
     * {@link AbilityView.describeBuffs} when you need the diagnostics for an
     * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks
     * the same eligibility set under target perspective.
     */
    buffsFor(input, context) {
        return this.collectBuffs(input, context, "attacker");
    }
    /**
     * Defender-perspective buff stack for the chosen unit: walks the same
     * eligible-abilities set as {@link buffsFor} but translates each ability's
     * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,
     * toughness mods from `stat-modifier T`, save rerolls, incoming hit
     * penalties from `bs-modifier`). Use this when the chosen unit is being
     * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/
     * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`
     * just means concatenating onto the existing `buffs` array.
     *
     * `weaponProfiles` are ignored under target perspective — weapon-keyword
     * effects ride with the firing weapon, not the receiving unit.
     */
    defensiveBuffsFor(input, context) {
        return this.collectBuffs(input, context, "target");
    }
    /**
     * Enumerate every attacker-side buff a unit could stack in `context` as a
     * list of toggleable levers, plus the activation groups that limit them.
     *
     * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —
     * this surfaces the *player decisions* too: stratagems, and the activatable
     * gates the DSL models as dice-pool options, `choice` branches, or
     * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).
     * Each lever carries `enabled` (its default state) and, where it's part of a
     * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many
     * can fire at once. The intended loop:
     *
     * ```ts
     * const { buffs } = ds.stackableBuffsFor(input, ctx);
     * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);
     * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);
     * ```
     *
     * Target/phase conditions a lever still carries (e.g. "vs Infantry") ride on
     * each buff's `applicableWhen`, so toggling it on is always safe — the
     * resolver gates it per-target.
     */
    stackableBuffsFor(input, context) {
        const buffs = [];
        const groups = new Map();
        // Surface the attachment fact to the DSL translator so `is-attached` /
        // `model-is-leader` conditions can evaluate. Clone — never mutate the
        // caller's context. An explicitly-set flag wins over the derivation.
        const ctx = {
            ...context,
            attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
        };
        // Intrinsic weapon-profile keywords — always on.
        for (const ref of input.weaponProfiles ?? []) {
            const weapon = this.weapons.get(ref.weaponId);
            if (!weapon)
                continue;
            const wk = weapon.profileBuffs(ref.profileIndex, ctx);
            if (wk.length === 0)
                continue;
            buffs.push({
                id: `weapon:${ref.weaponId}:${ref.profileIndex}`,
                label: `${weapon.name} keywords`,
                buffs: wk,
                enabled: true,
                source: wk[0].source,
            });
        }
        for (const entry of this.eligibleAbilities(input, ctx.phase)) {
            const source = bufferSourceFromEligible(entry);
            const { applied, activatable } = entry.ability.describeBuffs(source, ctx, "attacker");
            // Stratagems cost CP — opt-in, not on by default.
            const isStratagem = entry.source.kind === "detachment-stratagem";
            if (applied.length > 0) {
                buffs.push({
                    id: `${entry.source.kind}:${entry.ability.id}`,
                    label: entry.ability.name,
                    buffs: applied,
                    enabled: !isStratagem,
                    source,
                });
            }
            for (const act of activatable) {
                let groupId;
                if (act.group) {
                    groupId = act.group.id;
                    if (!groups.has(groupId)) {
                        groups.set(groupId, {
                            id: groupId,
                            label: entry.ability.name,
                            maxActivations: act.group.maxActivations,
                        });
                    }
                }
                buffs.push({
                    id: act.id,
                    label: `${entry.ability.name} — ${act.label}`,
                    buffs: act.buffs,
                    enabled: false,
                    source,
                    group: groupId,
                });
            }
        }
        return { buffs, groups: [...groups.values()] };
    }
    /** Shared implementation for buffsFor / defensiveBuffsFor. */
    collectBuffs(input, context, perspective) {
        const out = [];
        // Surface the attachment fact to the DSL translator (see stackableBuffsFor).
        // Clone — never mutate the caller's context; explicit flag wins.
        const ctx = {
            ...context,
            attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
        };
        // Weapon-profile keywords are attacker-only.
        if (perspective === "attacker") {
            for (const ref of input.weaponProfiles ?? []) {
                const weapon = this.weapons.get(ref.weaponId);
                if (!weapon)
                    continue;
                out.push(...weapon.profileBuffs(ref.profileIndex, ctx));
            }
        }
        const optedIn = new Set(input.optedInStratagemIds ?? []);
        for (const entry of this.eligibleAbilities(input, ctx.phase)) {
            if (entry.source.kind === "detachment-stratagem" && !optedIn.has(entry.source.stratagemId)) {
                continue;
            }
            const source = bufferSourceFromEligible(entry);
            out.push(...entry.ability.getBuffs(source, ctx, perspective));
        }
        return out;
    }
    buildIndexes(raw) {
        for (const pm of raw.phaseMappings) {
            const key = `${pm.source_type}:${pm.source_id}`;
            const existing = this.phaseIndex.get(key) ?? [];
            for (const phase of pm.phases) {
                if (!existing.includes(phase))
                    existing.push(phase);
            }
            this.phaseIndex.set(key, existing);
        }
        for (const unit of raw.units) {
            for (const abilityId of unit.ability_ids ?? [])
                push(this.unitsByAbility, abilityId, unit);
            for (const weaponId of unit.weapon_ids ?? [])
                push(this.unitsByWeapon, weaponId, unit);
        }
        for (const option of raw.wargearOptions) {
            push(this.wargearOptionsByUnit, option.unit_id, option);
        }
        const seenByKeyword = new Map();
        for (const weapon of raw.weapons) {
            for (const profile of weapon.profiles) {
                for (const ref of profile.keywords ?? []) {
                    let seen = seenByKeyword.get(ref.keyword_id);
                    if (!seen) {
                        seen = new Set();
                        seenByKeyword.set(ref.keyword_id, seen);
                    }
                    if (seen.has(weapon.id))
                        continue;
                    seen.add(weapon.id);
                    push(this.weaponsByKeyword, ref.keyword_id, weapon);
                }
            }
        }
    }
}
/** Build a passthrough collection for an id-bearing record type. */
function idCollection(items, factionOf) {
    return new Collection({
        items,
        idOf: (i) => i.id,
        nameOf: (i) => i.name,
        factionOf,
        wrap: (i) => i,
    });
}
function push(map, key, value) {
    const existing = map.get(key);
    if (existing)
        existing.push(value);
    else
        map.set(key, [value]);
}
/** Map an EligibleAbility back to the BuffSource the translator expects. */
function bufferSourceFromEligible(entry) {
    const abilityId = entry.ability.id;
    switch (entry.source.kind) {
        case "army":
            return { kind: "ability", abilityId, abilityKind: "army" };
        case "detachment":
            return { kind: "ability", abilityId, abilityKind: "detachment" };
        case "detachment-stratagem":
            return { kind: "ability", abilityId, abilityKind: "detachment-stratagem" };
        case "unit":
            return { kind: "ability", abilityId, abilityKind: "unit" };
        case "attached":
            return {
                kind: "ability",
                abilityId,
                abilityKind: "attached",
                sourceUnitId: entry.source.unitId,
            };
        case "support":
            return { kind: "ability", abilityId, abilityKind: "support" };
    }
}
//# sourceMappingURL=dataset.js.map