/**
 * Orchestrates an army-list import: decode → parse → resolve.
 *
 * The adapter seam ({@link FormatAdapter}) lets every supported source format
 * plug in here without touching {@link decode} or {@link resolve}. Adapters are
 * registered in priority order, and every adapter's `matches()` predicate is
 * tight enough that **at most one** matches any given decoded payload —
 * {@link tryImportRoster} relies on that disjointness to short-circuit on the
 * first match.
 *
 * @packageDocumentation
 */
import { Dataset } from "../data/dataset.js";
import { selectAdapter } from "./adapter.js";
import { decodeListForge } from "./decode.js";
import { gwAdapter } from "./gw.js";
import { listForgeAdapter } from "./listforge.js";
import { newRecruitJsonAdapter } from "./newrecruit-json.js";
import { newRecruitSimpleAdapter } from "./newrecruit-simple.js";
import { newRecruitWtcCompactAdapter, newRecruitWtcFullAdapter, } from "./newrecruit-wtc.js";
import { resolve } from "./resolve.js";
import { rosterizerAdapter } from "./rosterizer.js";
/**
 * Adapters available to {@link importRoster}, in match-priority order.
 *
 * NewRecruit-JSON runs ahead of ListForge because both recognise a
 * `roster.forces` BattleScribe payload, and the NewRecruit signature is more
 * specific (`xmlns: rosterSchema` or `generatedBy: newrecruit.eu`). The text
 * adapters (`gw` / `wtc-compact` / `wtc-full` / `simple`) only match strings and
 * disambiguate among themselves via structural cues, so their order amongst
 * each other doesn't matter; wtc-full goes before wtc-compact because its
 * matcher is the more specific of the two. GW shares the WTC summary header but
 * carries `•` bullets and no `N with` lines, so it stays disjoint from both wtc
 * matchers. Rosterizer rides at the top of the JSON dispatch — its `rulebook` +
 * `snapshot` signature is structurally distinct from the BattleScribe
 * `roster.forces` shape.
 */
const ADAPTERS = [
    rosterizerAdapter,
    newRecruitJsonAdapter,
    gwAdapter,
    newRecruitWtcFullAdapter,
    newRecruitWtcCompactAdapter,
    newRecruitSimpleAdapter,
    listForgeAdapter,
];
/**
 * Import a ListForge share payload into a resolved 40kdc {@link Roster}.
 *
 * `input` may be a full ListForge URL, a bare base64 segment, or an
 * already-decoded JSON string — all are handled transparently. For NewRecruit
 * sources, use {@link importNewRecruit} (no base64/gzip decode).
 */
export function importListForge(input, opts = {}) {
    const decoded = decodeListForge(input);
    return importRoster(decoded, opts);
}
/**
 * Import a NewRecruit export (any of the four formats — JSON, wtc-compact,
 * wtc-full, simple) into a resolved 40kdc {@link Roster}.
 *
 * The JSON form is parsed when `input` is valid JSON; the text forms are
 * dispatched on string content. No base64/gzip decoding is attempted —
 * NewRecruit exports are not encoded.
 */
export function importNewRecruit(input, opts = {}) {
    const trimmed = input.trimStart();
    if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
        try {
            return importRoster(JSON.parse(input), opts);
        }
        catch {
            // Fall through to treating the input as raw text.
        }
    }
    return importRoster(input, opts);
}
/**
 * Import an already-decoded payload. Selects the matching format adapter and
 * resolves the result against the dataset. Accepts either a parsed JSON object
 * (NewRecruit JSON / ListForge) or a string (the three NewRecruit text formats).
 */
/**
 * Detect an already-resolved canonical {@link Roster} (the JSON shape produced
 * by `rosterJsonSerializer`). Lets a downstream consumer round-trip canonical
 * Roster JSON through `importRoster` without going through an adapter.
 */
function isCanonicalRoster(decoded) {
    if (typeof decoded !== "object" || decoded === null)
        return false;
    const r = decoded;
    const source = r.source;
    return (typeof source === "object" &&
        source !== null &&
        typeof source.format === "string" &&
        Array.isArray(r.units) &&
        "diagnostics" in r);
}
export function importRoster(decoded, opts = {}) {
    if (isCanonicalRoster(decoded))
        return decoded;
    const ds = opts.dataset ?? Dataset.embedded();
    const adapter = selectAdapter(decoded, [...ADAPTERS]);
    const parsed = adapter.parse(decoded);
    return resolve(parsed, ds, adapter.id);
}
/** Cheap predicate: does the input look like ListForge's URL-or-base64 wrapper? */
function looksLikeListForgeEncoded(input) {
    if (input.includes("/listforge/"))
        return true;
    if (/^https?:\/\//i.test(input))
        return true;
    // Every gzip-then-base64 payload starts with this prefix.
    if (input.startsWith("H4sIA"))
        return true;
    return false;
}
/**
 * Auto-detect and import any supported roster format from a single string.
 *
 * Pipeline:
 * 1. Empty input → `empty-input`.
 * 2. Looks like a ListForge URL / base64 payload → decode (base64 + gunzip + JSON.parse).
 * 3. Looks like raw JSON (starts with `{`/`[`) → JSON.parse.
 * 4. Otherwise treat as text.
 * 5. Greedy first-match adapter dispatch. The first adapter whose `matches()`
 *    accepts the decoded value wins; subsequent adapters are not tried.
 * 6. If the matched adapter's `parse()` throws, that's a matcher contract
 *    violation — surfaced as `parse-failed`, not silently retried.
 *
 * Caller never sees an exception; the discriminated {@link ImportResult} carries
 * either the resolved {@link Roster} (with the detected {@link RosterFormat})
 * or a typed failure plus per-adapter trial info for diagnostics.
 *
 * Prefer this over {@link importListForge} / {@link importNewRecruit} when the
 * caller doesn't know which format the user pasted.
 */
export function tryImportRoster(input, opts = {}) {
    const trimmed = input.trim();
    if (trimmed === "") {
        return { ok: false, reason: "empty-input", message: "input is empty", trials: [] };
    }
    let decoded;
    if (looksLikeListForgeEncoded(trimmed)) {
        try {
            decoded = decodeListForge(trimmed);
        }
        catch (err) {
            const message = err.message;
            return {
                ok: false,
                reason: "decode-failed",
                message: `failed to decode ListForge payload: ${message}`,
                trials: [{ id: "listforge", matched: false, reason: message }],
            };
        }
    }
    else if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
        try {
            decoded = JSON.parse(trimmed);
        }
        catch (err) {
            return {
                ok: false,
                reason: "decode-failed",
                message: `input looks like JSON but failed to parse: ${err.message}`,
                trials: [],
            };
        }
    }
    else {
        decoded = input;
    }
    const ds = opts.dataset ?? Dataset.embedded();
    const trials = [];
    for (const adapter of ADAPTERS) {
        if (!adapter.matches(decoded)) {
            trials.push({ id: adapter.id, matched: false });
            continue;
        }
        try {
            const parsed = adapter.parse(decoded);
            const roster = resolve(parsed, ds, adapter.id);
            return { ok: true, roster, format: adapter.id };
        }
        catch (err) {
            const message = err.message;
            trials.push({ id: adapter.id, matched: true, reason: message });
            return {
                ok: false,
                reason: "parse-failed",
                message: `${adapter.id}: ${message}`,
                trials,
            };
        }
    }
    return {
        ok: false,
        reason: "no-adapter-matched",
        message: `tried ${ADAPTERS.length} formats, none recognised the input`,
        trials,
    };
}
/** The adapter list, exposed for tests that need to walk every matcher (e.g.
 * the disjointness invariant test). */
export const REGISTERED_ADAPTERS = ADAPTERS;
//# sourceMappingURL=import-roster.js.map