import { classifyWargearList, factionFromKeyword, inferBattleSizeRaw, splitWargearList, stripParenthetical, } from "./newrecruit-text.js";
const WTC_HEADER_PREFIX = "+ FACTION KEYWORD:";
const HEADER_FIELDS = {
    faction: /^\+\s*FACTION KEYWORD:\s*(.+?)\s*$/i,
    detachment: /^\+\s*DETACHMENT:\s*(.+?)\s*$/i,
    totalPoints: /^\+\s*TOTAL ARMY POINTS:\s*(\d+)\s*pts?\s*$/i,
    pointsLimit: /^\+\s*POINTS LIMIT:\s*(\d+)\s*pts?\s*$/i,
    listName: /^\+\s*LIST NAME:\s*(.+?)\s*$/i,
};
/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */
function parseWtcHeader(text) {
    const lines = text.split(/\r?\n/);
    let faction_raw_name = null;
    let detachment_raw_name = null;
    let totalReported = null;
    let pointsLimit = null;
    let listName = null;
    // Two `+++++…` fence lines wrap the header. Find them.
    const fenceIndices = [];
    for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {
        if (/^\++\s*$/.test(lines[i]))
            fenceIndices.push(i);
    }
    let sawFactionKeyword = false;
    for (const line of lines) {
        if (!line.startsWith("+"))
            continue;
        const factionMatch = HEADER_FIELDS.faction.exec(line);
        if (factionMatch) {
            faction_raw_name = factionFromKeyword(factionMatch[1]);
            sawFactionKeyword = true;
            continue;
        }
        const detMatch = HEADER_FIELDS.detachment.exec(line);
        if (detMatch) {
            detachment_raw_name = stripParenthetical(detMatch[1]);
            continue;
        }
        const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);
        if (ptsMatch) {
            totalReported = Number.parseInt(ptsMatch[1], 10);
            continue;
        }
        const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);
        if (limitMatch) {
            pointsLimit = Number.parseInt(limitMatch[1], 10);
            continue;
        }
        const nameMatch = HEADER_FIELDS.listName.exec(line);
        if (nameMatch) {
            listName = nameMatch[1];
        }
    }
    if (!sawFactionKeyword)
        return null;
    const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;
    // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —
    // is the army's points ceiling. When the source carries only a single
    // figure (the tournament default), fall back to it.
    const declared_limit = pointsLimit ?? totalReported;
    const battle_size_raw = inferBattleSizeRaw(declared_limit);
    return {
        header: {
            name: listName ?? "Imported roster",
            faction_raw_name,
            detachment_raw_name,
            declared_limit,
            total_reported: totalReported,
            battle_size_raw,
        },
        bodyStart,
    };
}
// --- shared body helpers ----------------------------------------------------
const UNIT_HEADER_COMPACT = /^(?:Char\d+:\s*)?(\d+)x\s+(.+?)\s*\(\s*(\d+)\s*pts?\s*\)\s*:\s*(.*)$/i;
const UNIT_HEADER_FULL = /^(?:Char\d+:\s*)?(\d+)x\s+(.+?)\s*\(\s*(\d+)\s*pts?\s*\)\s*$/i;
const ENHANCEMENT_LINE = /^Enhancement:\s*(.+?)\s*\(\+\s*(\d+)\s*pts?\s*\)\s*$/i;
const WITH_PREFIX = /^(\d+)\s+with\s+(.*)$/i;
const MODEL_BREAKDOWN = /^\s*•\s*(\d+)x\s+(.+?)(?:\s*\[[^\]]*\])?\s*$/u;
const SECTION_HEADER = /^[A-Z][A-Z0-9 \-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.
const HEADER_LINE = /^\+/;
/**
 * `N with X, Y, Z` means each of `N` models carries the same list — the weapon
 * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when
 * the line has no `with` prefix.
 */
function parseWithGroup(text) {
    const m = WITH_PREFIX.exec(text);
    if (m) {
        const n = Number.parseInt(m[1], 10);
        return { multiplier: n > 0 ? n : 1, list: m[2] };
    }
    return { multiplier: 1, list: text };
}
function newUnit(name, displayed_pts, leading_count, is_character_prefix) {
    return {
        raw_name: name,
        is_character: is_character_prefix,
        is_warlord: false,
        enhancement_raw_name: null,
        displayed_pts,
        enhancement_pts: 0,
        model_count: leading_count > 0 ? leading_count : 1,
        wargear: new Map(),
    };
}
function addWargear(unit, items) {
    for (const { raw_name, count } of items) {
        unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);
    }
}
function applyWithGroup(unit, listText) {
    const { multiplier, list } = parseWithGroup(listText);
    const tokens = splitWargearList(list);
    const cls = classifyWargearList(tokens);
    if (cls.is_warlord)
        unit.is_warlord = true;
    if (cls.is_character)
        unit.is_character = true;
    // wtc never inlines the enhancement points in the wargear list (that's the
    // simple format) but classifyWargearList silently absorbs it if it shows up;
    // wtc's enhancement is always parsed off the explicit "Enhancement:" line.
    const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));
    addWargear(unit, scaled);
}
function finishUnit(unit) {
    const displayed = unit.displayed_pts;
    const points = displayed === null ? null : displayed - unit.enhancement_pts;
    const wargear = [];
    for (const [raw_name, count] of unit.wargear) {
        wargear.push({ raw_name, count });
    }
    return {
        raw_name: unit.raw_name,
        is_character: unit.is_character,
        model_count: unit.model_count,
        points,
        is_warlord: unit.is_warlord,
        enhancement_raw_name: unit.enhancement_raw_name,
        enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,
        wargear,
    };
}
/** Compute total_computed by walking every parsed unit cost line. */
function computeTotal(units, enhancementPtsByIndex) {
    let total = 0;
    for (let i = 0; i < units.length; i += 1) {
        total += units[i].points ?? 0;
        total += enhancementPtsByIndex[i] ?? 0;
    }
    return total;
}
function attachEnhancement(unit, raw_name, pts) {
    unit.enhancement_raw_name = raw_name.trim();
    unit.enhancement_pts = pts;
}
// --- compact body parser ----------------------------------------------------
function parseCompactBody(body) {
    const lines = body.split(/\r?\n/);
    const units = [];
    const enhancementPts = [];
    let current = null;
    const finalize = () => {
        if (current) {
            units.push(finishUnit(current));
            enhancementPts.push(current.enhancement_pts);
            current = null;
        }
    };
    for (const raw of lines) {
        const line = raw.trim();
        if (!line || HEADER_LINE.test(line) || /^\++$/.test(line))
            continue;
        const enhMatch = ENHANCEMENT_LINE.exec(line);
        if (enhMatch && current) {
            attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));
            // Emit immediately so subsequent unit lines start fresh.
            finalize();
            continue;
        }
        const unitMatch = UNIT_HEADER_COMPACT.exec(line);
        if (unitMatch) {
            finalize();
            const leading_count = Number.parseInt(unitMatch[1], 10);
            const name = unitMatch[2].trim();
            const pts = Number.parseInt(unitMatch[3], 10);
            const is_character_prefix = /^Char\d+:/i.test(line);
            current = newUnit(name, pts, leading_count, is_character_prefix);
            applyWithGroup(current, unitMatch[4]);
            continue;
        }
    }
    finalize();
    return { units, enhancementPts };
}
// --- full body parser -------------------------------------------------------
function parseFullBody(body) {
    const lines = body.split(/\r?\n/);
    const units = [];
    const enhancementPts = [];
    let current = null;
    let breakdownModels = 0;
    const finalize = () => {
        if (current) {
            if (breakdownModels > 0)
                current.model_count = breakdownModels;
            units.push(finishUnit(current));
            enhancementPts.push(current.enhancement_pts);
            current = null;
            breakdownModels = 0;
        }
    };
    for (const raw of lines) {
        const line = raw.trim();
        if (!line || HEADER_LINE.test(line) || /^\++$/.test(line))
            continue;
        if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {
            finalize();
            continue;
        }
        const enhMatch = ENHANCEMENT_LINE.exec(line);
        if (enhMatch && current) {
            attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));
            continue;
        }
        const unitMatch = UNIT_HEADER_FULL.exec(line);
        if (unitMatch) {
            finalize();
            const leading_count = Number.parseInt(unitMatch[1], 10);
            const name = unitMatch[2].trim();
            const pts = Number.parseInt(unitMatch[3], 10);
            const is_character_prefix = /^Char\d+:/i.test(line);
            current = newUnit(name, pts, leading_count, is_character_prefix);
            continue;
        }
        const breakdown = MODEL_BREAKDOWN.exec(raw);
        if (breakdown && current) {
            breakdownModels += Number.parseInt(breakdown[1], 10);
            continue;
        }
        if (WITH_PREFIX.test(line) && current) {
            applyWithGroup(current, line);
            continue;
        }
    }
    finalize();
    return { units, enhancementPts };
}
// --- multi-force detection --------------------------------------------------
/** Heuristic for `multi_force`: are there units with "ALLIED" decorating
 * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact
 * has no section markers but the user-facing summary header counts every unit
 * together, so detect from explicit section presence. */
function detectMultiForce(text, format) {
    if (format === "wtc-full") {
        return /^ALLIED UNITS\s*$/im.test(text);
    }
    // wtc-compact has no section header. Multi-force surfaces only via the
    // primary-faction summary; assume single-force unless we add a richer marker.
    return false;
}
// --- adapters ---------------------------------------------------------------
function isWtcText(decoded) {
    if (typeof decoded !== "string")
        return null;
    // Both wtc formats begin with the FACTION KEYWORD header line (possibly
    // after some leading whitespace/fence characters).
    if (!decoded.includes(WTC_HEADER_PREFIX))
        return null;
    return decoded;
}
/** Distinguishes wtc-full from wtc-compact: full has a line starting with
 * `\d+ with ` at the start of a body line (compact only puts `N with` after
 * `:` on the same line as the unit header). */
function isFullFormat(text) {
    return /^[\t ]*\d+\s+with\b/m.test(text);
}
/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW
 * app format uses them for every wargear entry. wtc-compact never emits them,
 * so it's the one matcher that must exclude them to stay disjoint from GW. */
function hasBullets(text) {
    return /^[\t ]*•/mu.test(text);
}
function parseWith(text, format) {
    const parsed = parseWtcHeader(text);
    if (!parsed) {
        throw new Error(`${format}: missing "+ FACTION KEYWORD:" header`);
    }
    const { header, bodyStart } = parsed;
    const body = text.split(/\r?\n/).slice(bodyStart).join("\n");
    const { units, enhancementPts } = format === "wtc-full" ? parseFullBody(body) : parseCompactBody(body);
    return {
        name: header.name,
        generated_by: null,
        faction_raw_name: header.faction_raw_name,
        detachment_raw_name: header.detachment_raw_name,
        battle_size_raw: header.battle_size_raw,
        declared_limit: header.declared_limit,
        total_reported: header.total_reported,
        total_computed: computeTotal(units, enhancementPts),
        units,
        multi_force: detectMultiForce(text, format),
    };
}
export const newRecruitWtcCompactAdapter = {
    id: "newrecruit-wtc-compact",
    matches(decoded) {
        const text = isWtcText(decoded);
        if (text === null)
            return false;
        // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets
        // (that's the GW app format) — excluding both keeps the matcher disjoint.
        return !isFullFormat(text) && !hasBullets(text);
    },
    parse(decoded) {
        const text = isWtcText(decoded);
        if (text === null)
            throw new Error("newrecruit-wtc-compact: input is not a string");
        return parseWith(text, "wtc-compact");
    },
};
export const newRecruitWtcFullAdapter = {
    id: "newrecruit-wtc-full",
    matches(decoded) {
        const text = isWtcText(decoded);
        if (text === null)
            return false;
        return isFullFormat(text);
    },
    parse(decoded) {
        const text = isWtcText(decoded);
        if (text === null)
            throw new Error("newrecruit-wtc-full: input is not a string");
        return parseWith(text, "wtc-full");
    },
};
//# sourceMappingURL=newrecruit-wtc.js.map