Torn War Targets

Adds a box with possible targets to faction page

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn War Targets
// @namespace    https://www.torn.com/factions.php
// @version      v1.6.6
// @description  Adds a box with possible targets to faction page
// @author       Maahly [3893095]
// @match        https://www.torn.com/factions.php?step=your*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM.xmlHttpRequest
// @connect      api.torn.com
// @connect      ffscouter.com
// ==/UserScript==

// Feel free to modify these values
const DEFAULT_MIN_FAIR_FIGHT = 0;
const DEFAULT_MAX_FAIR_FIGHT = 3.5;
const LAST_N_MESSAGES_TO_CHECK_FOR_DIBS = 15;
const CALL_FULFILLMENT_TIMEOUT_MINUTES = 15;
const TARGET_REFRESH_INTERVAL_NOT_IN_WAR_MS = 15000;
const TARGET_REFRESH_INTERVAL_IN_WAR_MS = 5000;
const ENABLE_DEBUG_LOGS = false;

// /////////////////////////////
//
// DO NOT TOUCH BELOW THIS POINT
//
// /////////////////////////////

const FFSCOUTER_KEY_LENGTH = 16;
const MIN_CALL_FRAGMENT_LENGTH = 4;
const FFSCOUTER_API_KEY_STORAGE_KEY = 'ffscouterApiKey';
const CONTENT_ELEMENT_ID = 'war-tagets-content';
const HEADER_ELEMENT_ID = 'war-targets-header';
const HEADER_TITLE_CLASS = 'war-targets-header-title';
const NO_ACTIVE_WAR_MESSAGE = 'NO ONGOING WAR';
const TARGET_STYLE_ID = 'war-targets-style';
const CALL_FULFILLMENT_TIMEOUT_MS = CALL_FULFILLMENT_TIMEOUT_MINUTES * 60 * 1000;
const LAST_UPDATED_INTERVAL_MS = 1000;
const REQUEST_TIMEOUT_MS = 10000;
const FACTION_CHAT_ID_PATTERN = /^faction-\d+$/;
const headerState = {
    lastUpdatedAt: null,
    lastUpdatedTimer: null,
    refreshTimer: null,
    warStarted: false,
    isCollapsed: true,
    noActiveWar: false,
};
const targetState = {
    cards: new Map(),
    grid: null,
    message: null,
    settingsPanel: null,
    minFairFightInput: null,
    maxFairFightInput: null,
    onlineFirstInput: null,
    calledFragments: new Set(),
    calledExactNames: new Set(),
    calledBy: new Map(),
    calledByExactName: new Map(),
    selfCalledFragments: new Set(),
    selfCalledExactNames: new Set(),
    callEntries: [],
    parsedMessages: new Map(),
    lastKnownStates: new Map(),
    wrapper: null,
    latestTargets: [],
    minFairFight: DEFAULT_MIN_FAIR_FIGHT,
    maxFairFight: DEFAULT_MAX_FAIR_FIGHT,
    onlineFirst: false,
};
const chatState = {
    listObserver: null,
    listElement: null,
};
const statsCache = new Map();
const LOG_PREFIX = '[Torn War Targets]';
const logInfo = (...args) => console.log(LOG_PREFIX, ...args);
const logWarn = (...args) => console.warn(LOG_PREFIX, ...args);
const logError = (...args) => console.error(LOG_PREFIX, ...args);
const logDebug = (...args) => {
    if (!ENABLE_DEBUG_LOGS) {
        return;
    }
    console.debug(LOG_PREFIX, ...args);
};
const isFunction = (value) => typeof value === 'function';
const getModernGmApi = () => globalThis.GM ?? null;
const getTornPdaHttpGet = () =>
    globalThis.PDA_httpGet ?? globalThis.unsafeWindow?.PDA_httpGet ?? null;

const safeGetValue = (key, fallback = '') => {
    if (!isFunction(globalThis.GM_getValue)) {
        try {
            return globalThis.localStorage?.getItem(key) ?? fallback;
        } catch (error) {
            logWarn('Failed to read from localStorage.', { key, error });
            return fallback;
        }
    }

    try {
        return GM_getValue(key, fallback);
    } catch (error) {
        logWarn('GM_getValue failed.', { key, error });
        return fallback;
    }
};

const safeSetValue = (key, value) => {
    if (!isFunction(globalThis.GM_setValue)) {
        try {
            globalThis.localStorage?.setItem(key, value);
        } catch (error) {
            logWarn('Failed to write to localStorage.', { key, error });
        }
        return;
    }

    try {
        GM_setValue(key, value);
    } catch (error) {
        logWarn('GM_setValue failed.', { key, error });
    }
};

const getAvailabilityStatus = (member) => {
    if (!member) {
        return 'Offline';
    }

    return member?.last_update?.status ?? member?.last_action?.status ?? 'Offline';
};

const hasOwn = (value, property) =>
    value != null && Object.prototype.hasOwnProperty.call(value, property);
const isCompletedRankedWar = (war) => {
    const winner = war?.winner;
    if (winner != null && winner !== '') {
        return true;
    }

    const endValue = Number(war?.end);
    return Number.isFinite(endValue) && endValue > 0;
};
const toTimestampMs = (value) => {
    if (value == null || value === '') {
        return null;
    }
    const numeric = Number(value);
    if (Number.isFinite(numeric)) {
        if (numeric > 1e12) {
            return numeric;
        }
        if (numeric > 1e9) {
            return numeric * 1000;
        }
    }
    const parsedDate = Date.parse(value);
    return Number.isNaN(parsedDate) ? null : parsedDate;
};

const getHeaderTitle = ({ noActiveWar = false } = {}) => {
    const baseTitle = `My Targets`;
    if (!noActiveWar) {
        return `${baseTitle} | FF: ${targetState.minFairFight}-${targetState.maxFairFight}`;
    }
    return `${baseTitle} | ${NO_ACTIVE_WAR_MESSAGE}`;
};

const parseFairFightValue = (value, fallback) => {
    if (typeof value === 'string' && value.trim() === '') {
        return fallback;
    }
    const parsed = Number(value);
    if (!Number.isFinite(parsed)) {
        return fallback;
    }
    return Math.max(0, Math.round(parsed * 10) / 10);
};

const updateFairFightInputs = () => {
    if (targetState.minFairFightInput) {
        targetState.minFairFightInput.value = String(targetState.minFairFight);
    }
    if (targetState.maxFairFightInput) {
        targetState.maxFairFightInput.value = String(targetState.maxFairFight);
    }
};

const applyFairFightFilter = () => {
    const nextMin = parseFairFightValue(
        targetState.minFairFightInput?.value,
        DEFAULT_MIN_FAIR_FIGHT
    );
    const nextMax = parseFairFightValue(
        targetState.maxFairFightInput?.value,
        9999
    );
    targetState.minFairFight = Math.min(nextMin, nextMax);
    targetState.maxFairFight = Math.max(nextMin, nextMax);
    setNoActiveWarHeaderState(headerState.noActiveWar);
    if (targetState.latestTargets.length > 0) {
        renderTargetGrid(targetState.latestTargets);
    }
};

const applyOnlineFirstSetting = () => {
    targetState.onlineFirst = Boolean(targetState.onlineFirstInput?.checked);
    if (targetState.latestTargets.length > 0) {
        renderTargetGrid(targetState.latestTargets);
    }
};

const setNoActiveWarHeaderState = (isNoActiveWar) => {
    const header = document.getElementById(HEADER_ELEMENT_ID);
    const headerTitle = header?.querySelector(`.${HEADER_TITLE_CLASS}`);
    headerState.noActiveWar = Boolean(isNoActiveWar);
    if (headerTitle) {
        headerTitle.textContent = getHeaderTitle({ noActiveWar: isNoActiveWar });
    }

    const lastUpdated = header?.querySelector('.war-targets-last-updated');
    if (lastUpdated) {
        lastUpdated.style.display = isNoActiveWar ? 'none' : '';
        if (!isNoActiveWar && !headerState.lastUpdatedAt) {
            lastUpdated.textContent = 'Last updated: --';
        }
    }

    const content = document.getElementById(CONTENT_ELEMENT_ID);
    if (content && header) {
        const shouldHideContent = headerState.noActiveWar || headerState.isCollapsed;
        header.classList.toggle('active', !shouldHideContent);
        header.setAttribute('aria-expanded', String(!shouldHideContent));
        content.style.display = shouldHideContent ? 'none' : '';
    }
};

const toggleCollapsedState = () => {
    headerState.isCollapsed = !headerState.isCollapsed;
    setNoActiveWarHeaderState(headerState.noActiveWar);
};

const isFederalTarget = (target) => target?.status?.state === 'Federal';
const isTravelingTarget = (target) => target?.status?.state === 'Traveling';
const isAbroadTarget = (target) => target?.status?.state === 'Abroad';
const isAbroadHospitalTarget = (target) => {
    if (target?.status?.state !== 'Hospital') {
        return false;
    }
    return /In\s+a\s+.+\s+hospital/i.test(target?.status?.description ?? '');
};

const ensureTargetStyles = () => {
    if (document.getElementById(TARGET_STYLE_ID)) {
        return;
    }

    const style = document.createElement('style');
    style.id = TARGET_STYLE_ID;
    style.textContent = `
        #${HEADER_ELEMENT_ID} {
            cursor: pointer;
        }

        .war-targets-wrapper {
            display: flex;
            flex-direction: column;
            gap: 16px;
            padding: 12px;
            color: #f3f4f6;
            font-family: "Inter", "Segoe UI", sans-serif;
        }

        .war-targets-section-title {
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: 0.2em;
            color: #cbd5f5;
            margin-bottom: 6px;
        }

        .war-targets-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 8px;
        }

        .war-target-card {
            border-radius: 8px;
            border: 1px solid #1f2937;
            background: #111827;
            padding: 8px;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
            cursor: pointer;
            position: relative;
        }

        .war-target-card.state-okay {
            background: #064e3b;
            border-color: #047857;
        }

        .war-target-card.state-hospital {
            background: #7f1d1d;
            border-color: #dc2626;
        }

        .war-target-card.is-called {
            border-color: #fb7185;
            box-shadow: 0 0 0 1px rgba(190, 18, 60, 0.8),
                0 2px 6px rgba(190, 18, 60, 0.35);
        }

        .war-target-card.is-yours {
            border-color: #38bdf8;
            box-shadow: 0 0 0 1px rgba(14, 116, 144, 0.8),
                0 2px 6px rgba(14, 116, 144, 0.35);
        }

        .war-target-call-button {
            position: absolute;
            right: 6px;
            bottom: 6px;
            border: none;
            border-radius: 999px;
            padding: 4px 8px;
            font-size: 10px;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.12em;
            background: rgba(15, 23, 42, 0.9);
            color: #e2e8f0;
            cursor: pointer;
        }

        .war-target-call-button:hover {
            background: rgba(30, 41, 59, 0.95);
        }

        .war-target-called-badge {
            position: absolute;
            top: 6px;
            right: 6px;
            padding: 2px 6px;
            border-radius: 999px;
            background: rgba(190, 18, 60, 0.9);
            color: #fff1f2;
            font-size: 9px;
            letter-spacing: 0.16em;
            text-transform: uppercase;
            display: none;
        }

        .war-target-called-badge.is-yours {
            background: rgba(14, 116, 144, 0.9);
            color: #ecfeff;
        }

        .war-target-name {
            display: flex;
            align-items: center;
            gap: 6px;
            font-weight: 600;
            font-size: 12px;
            margin-bottom: 6px;
            cursor: pointer;
        }

        .war-target-status-dot {
            width: 8px;
            height: 8px;
            border-radius: 999px;
        }

        .war-target-status-dot.online {
            background: #22c55e;
        }

        .war-target-status-dot.idle {
            background: #facc15;
        }

        .war-target-status-dot.offline {
            background: #9ca3af;
        }

        .war-target-meta {
            font-size: 11px;
            line-height: 1.4;
            color: #f9fafb;
            cursor: pointer;
        }

        .war-target-ff {
            color: #fde047;
            font-weight: 600;
        }

        .war-target-divider {
            border-top: 1px solid rgba(255, 255, 255, 0.1);
            padding-top: 12px;
            margin-top: 12px;
        }

        .war-targets-header-content {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
        }

        .war-targets-last-updated {
            font-size: 11px;
            color: #cbd5f5;
            margin-left: 12px;
            padding-right: 6px;
            white-space: nowrap;
        }

        .war-targets-message {
            font-size: 12px;
            color: #e5e7eb;
        }

        .war-targets-settings {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 6px 8px;
            border: 1px solid #334155;
            border-radius: 6px;
            background: #0f172a;
            box-sizing: border-box;
            width: 100%;
            max-width: 100%;
        }

        .war-targets-settings label {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 11px;
            color: #cbd5f5;
            white-space: nowrap;
        }

        .war-targets-settings input {
            width: 56px;
            border: 1px solid #475569;
            border-radius: 4px;
            background: #020617;
            color: #f8fafc;
            font-size: 11px;
            padding: 3px 4px;
        }

        .war-targets-settings input[type='checkbox'] {
            width: auto;
            border: 0;
            border-radius: 0;
            background: transparent;
            padding: 0;
            accent-color: #38bdf8;
        }

        .war-targets-api-key-form {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 8px;
            margin-top: 8px;
        }

        .war-targets-api-key-input {
            min-width: 220px;
            width: min(320px, 100%);
            border: 1px solid #4b5563;
            border-radius: 6px;
            padding: 6px 8px;
            background: #0f172a;
            color: #f8fafc;
            font-size: 12px;
        }

        .war-targets-api-key-button {
            border: 1px solid #2563eb;
            border-radius: 6px;
            padding: 6px 10px;
            background: #1d4ed8;
            color: #eff6ff;
            cursor: pointer;
            font-size: 12px;
            font-weight: 600;
        }

        .war-targets-api-key-button:hover {
            background: #1e40af;
        }
    `;

    document.head.appendChild(style);
};

const getHospitalLabel = (untilTimestamp) => {
    const untilSeconds = Number(untilTimestamp);
    if (!Number.isFinite(untilSeconds) || untilSeconds <= 0) {
        return { label: '', remainingSeconds: null };
    }

    const remainingSeconds = Math.max(0, Math.floor(untilSeconds - Date.now() / 1000));
    if (remainingSeconds === 0) {
        return { label: '', remainingSeconds };
    }

    if (remainingSeconds < 60) {
        return { label: `${remainingSeconds}s`, remainingSeconds };
    }

    const hours = Math.floor(remainingSeconds / 3600);
    const minutes = Math.floor((remainingSeconds % 3600) / 60);
    const parts = [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null]
        .filter(Boolean)
        .join('');

    return { label: parts, remainingSeconds };
};

const findFactionChatElement = (node) => {
    if (!(node instanceof Element)) {
        return null;
    }

    if (FACTION_CHAT_ID_PATTERN.test(node.id)) {
        return node;
    }

    const candidates = node.querySelectorAll('[id^="faction-"]');
    for (const candidate of candidates) {
        if (FACTION_CHAT_ID_PATTERN.test(candidate.id)) {
            return candidate;
        }
    }

    return null;
};

const getFactionChatList = () => {
    const factionChat = document.querySelector('[id^="faction-"]');
    return factionChat?.querySelector('[class^="scrollWrapper__"]') ?? null;
};

const getFactionChatTextarea = () => {
    const factionChat = document.querySelector('[id^="faction-"]');
    return factionChat?.querySelector('[class^="textarea__"]') ?? null;
};

const startsWithAnyFragment = (targetName, fragments) => {
    if (!targetName) {
        return false;
    }
    const lowerName = targetName.toLowerCase();
    for (const fragment of fragments) {
        if (lowerName.startsWith(fragment)) {
            return true;
        }
    }
    return false;
};

const dispatchEnterKey = (element) => {
    if (!element) {
        return;
    }
    const eventInit = {
        key: 'Enter',
        code: 'Enter',
        keyCode: 13,
        which: 13,
        bubbles: true,
        cancelable: true,
    };
    element.dispatchEvent(new KeyboardEvent('keydown', eventInit));
    element.dispatchEvent(new KeyboardEvent('keypress', eventInit));
    element.dispatchEvent(new KeyboardEvent('keyup', eventInit));
};

const collectLastMessages = (listElement) => {
    if (!listElement) {
        logDebug('collectLastMessages skipped: missing chat list element.');
        return [];
    }

    const messageContainer = listElement.firstElementChild;
    if (!messageContainer) {
        logDebug('collectLastMessages skipped: message container is unavailable.');
        return [];
    }

    const messages = [...messageContainer.children]
        .slice(-LAST_N_MESSAGES_TO_CHECK_FOR_DIBS)
        .map((node) => node.innerText);
    logDebug('Collected recent faction chat messages.', {
        messageCount: messages.length,
        maxTrackedMessages: LAST_N_MESSAGES_TO_CHECK_FOR_DIBS,
    });
    return messages;
};

const observeChatList = (listElement) => {
    if (!listElement || chatState.listElement === listElement) {
        if (!listElement) {
            logDebug('observeChatList skipped: no list element available yet.');
        }
        return;
    }

    if (chatState.listObserver) {
        chatState.listObserver.disconnect();
    }

    chatState.listElement = listElement;
    chatState.listObserver = new MutationObserver((mutations) => {
        if (!mutations.some((mutation) => mutation.addedNodes.length > 0)) {
            return;
        }

        logDebug('Faction chat list mutation detected.', {
            mutationCount: mutations.length,
            listChildCount: listElement.childElementCount,
        });

        updateCalledTargets(collectLastMessages(listElement));
    });

    chatState.listObserver.observe(listElement, { childList: true, subtree: true });
    updateCalledTargets(collectLastMessages(listElement));
};

const observeChatRoot = (chatRoot) => {
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (findFactionChatElement(node)) {
                    logInfo('Faction chat opened.');
                    const listElement = getFactionChatList();
                    observeChatList(listElement);
                    return;
                }
            }
        }
    });

    observer.observe(chatRoot, { childList: true, subtree: true });
    observeChatList(getFactionChatList());
};

const startChatRootObserver = () => {
    const chatRoot = document.getElementById('chatRoot');
    if (!chatRoot) {
        logWarn('Chat root not found yet.');
        return false;
    }

    observeChatRoot(chatRoot);
    return true;
};

const waitForChatRoot = () => {
    if (startChatRootObserver()) {
        return;
    }

    const bodyObserver = new MutationObserver(() => {
        if (startChatRootObserver()) {
            bodyObserver.disconnect();
        }
    });

    if (!document.body) {
        logError('document.body is unavailable; cannot observe chat root.');
        return;
    }

    bodyObserver.observe(document.body, { childList: true, subtree: true });
};

const getHospitalRemainingSeconds = (untilTimestamp) => {
    const untilSeconds = Number(untilTimestamp);
    if (!Number.isFinite(untilSeconds) || untilSeconds <= 0) {
        return null;
    }
    const remaining = Math.floor(untilSeconds - Date.now() / 1000);
    return Math.max(0, remaining);
};

const isCallEligible = (target) => {
    if (target?.status?.state !== 'Hospital') {
        return true;
    }
    const remainingSeconds = getHospitalRemainingSeconds(target?.status?.until);
    if (remainingSeconds == null) {
        return true;
    }
    return remainingSeconds <= 20 * 60;
};

const getEffectiveState = (target) => {
    const rawState = target?.status?.state ?? 'Unknown';
    if (rawState !== 'Hospital') {
        return rawState;
    }
    const remaining = getHospitalRemainingSeconds(target?.status?.until);
    return remaining === 0 ? 'Okay' : rawState;
};

const getEffectiveDescription = (target, effectiveState) => {
    if (effectiveState === 'Okay' && target?.status?.state === 'Hospital') {
        return '';
    }
    return target?.status?.description ?? '';
};

const formatCallMessage = (target) => {
    const targetName = target?.name ?? '';
    if (!targetName) {
        return '';
    }
    const effectiveState = getEffectiveState(target);
    if (effectiveState !== 'Hospital') {
        return targetName;
    }
    const remainingSeconds = getHospitalRemainingSeconds(target?.status?.until);
    if (remainingSeconds == null || remainingSeconds <= 0) {
        return targetName;
    }
    const minutesRemaining = Math.max(1, Math.ceil(remainingSeconds / 60));
    return `${targetName} in ${minutesRemaining}`;
};

const getStatusLabel = ({ state, description, hospitalLabel }) => {
    if (state === 'Hospital' && hospitalLabel) {
        const isAbroadHospital = /In\s+a\s+.+\s+hospital/i.test(description);
        return `${isAbroadHospital ? 'Hosp' : 'Hospital'} ${hospitalLabel}`;
    }

    if (state === 'Traveling' && description.startsWith('Traveling')) {
        return description;
    }

    return state;
};

const updateTargetCard = (target, cardData) => {
    if (!cardData?.card) {
        return;
    }

    cardData.targetData = target;
    const effectiveState = getEffectiveState(target);
    const effectiveDescription = getEffectiveDescription(target, effectiveState);
    const { label: hospitalLabel } = getHospitalLabel(target?.status?.until);
    const stateClass = (effectiveState ?? 'unknown').toLowerCase();
    cardData.card.className = `war-target-card state-${stateClass}`;
    const targetId = String(target?.id ?? '');
    const targetName = target?.name ?? '';
    handleTargetStateTransition(targetId, targetName, effectiveState);
    applyCalledState(cardData, targetName);
    cardData.card.dataset.targetId = targetId;

    const availability = (target?.availability_status ?? 'Offline').toLowerCase();
    cardData.statusDot.className = `war-target-status-dot ${availability}`;
    cardData.statusDot.title = availability;

    cardData.name.textContent = target?.name ?? 'Unknown';
    cardData.statusLine.textContent = getStatusLabel({
        state: effectiveState,
        description: effectiveDescription,
        hospitalLabel,
    });
    cardData.bsLine.textContent = `BS: ${target?.bs_estimate_human ?? 'Unknown'}`;
    cardData.ffLine.className = 'war-target-ff';
    cardData.ffLine.textContent = `FF: ${target?.fair_fight ?? 'N/A'}`;
    updateCalledBadge(cardData, targetName);
    updateCallButtonVisibility(cardData, targetName);
};

const renderTargetCard = (target) => {
    const card = document.createElement('div');
    const cardData = {
        card,
        name: document.createElement('span'),
        statusDot: document.createElement('span'),
        statusLine: document.createElement('div'),
        bsLine: document.createElement('div'),
        ffLine: document.createElement('div'),
        calledBadge: document.createElement('div'),
        callButton: document.createElement('button'),
    };
    updateTargetCard(target, cardData);

    const targetId = target?.id;
    if (targetId) {
        card.addEventListener('click', () => {
            window.open(
                `https://www.torn.com/loader.php?sid=attack&user2ID=${encodeURIComponent(targetId)}`,
                '_blank',
                'noopener'
            );
        });
    }

    const nameRow = document.createElement('div');
    nameRow.className = 'war-target-name';

    nameRow.appendChild(cardData.statusDot);
    nameRow.appendChild(cardData.name);

    const meta = document.createElement('div');
    meta.className = 'war-target-meta';

    meta.appendChild(cardData.statusLine);
    meta.appendChild(cardData.bsLine);
    meta.appendChild(cardData.ffLine);

    cardData.calledBadge.className = 'war-target-called-badge';
    cardData.callButton.className = 'war-target-call-button';
    cardData.callButton.type = 'button';
    cardData.callButton.textContent = 'Call';
    cardData.callButton.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        const textarea = getFactionChatTextarea();
        if (!textarea) {
            logWarn('Cannot send call message: faction chat textarea not found.', {
                targetName: cardData?.targetData?.name,
            });
            window.alert('Open the faction chat first.');
            return;
        }
        const message = formatCallMessage(cardData?.targetData);
        if (!message) {
            logWarn('Cannot send call message: empty message after formatting.', {
                target: cardData?.targetData,
            });
            return;
        }
        textarea.value = message;
        textarea.dispatchEvent(new Event('input', { bubbles: true }));
        textarea.focus();
        setTimeout(() => {
            dispatchEnterKey(textarea);
        }, 0);
    });
    card.appendChild(cardData.calledBadge);
    card.appendChild(cardData.callButton);
    card.appendChild(nameRow);
    card.appendChild(meta);

    return cardData;
};

const isTargetCalled = (targetName) => {
    if (!targetName) {
        return false;
    }
    const lowerName = targetName.toLowerCase();
    if (targetState.calledExactNames.has(lowerName)) {
        return true;
    }
    return startsWithAnyFragment(targetName, targetState.calledFragments);
};

const getTargetCaller = (targetName) => {
    if (!targetName) {
        return null;
    }
    const lowerName = targetName.toLowerCase();
    if (targetState.calledByExactName.has(lowerName)) {
        return targetState.calledByExactName.get(lowerName) ?? null;
    }
    for (const fragment of targetState.calledFragments) {
        if (lowerName.startsWith(fragment)) {
            return targetState.calledBy.get(fragment) ?? null;
        }
    }
    return null;
};

const isTargetCalledBySelf = (targetName) => {
    if (!targetName) {
        return false;
    }
    const lowerName = targetName.toLowerCase();
    if (targetState.selfCalledExactNames.has(lowerName)) {
        return true;
    }
    return startsWithAnyFragment(targetName, targetState.selfCalledFragments);
};

const formatCallerLabel = (name) => {
    if (!name) {
        return 'Called';
    }
    const trimmed = name.trim();
    if (trimmed.length <= 10) {
        return trimmed;
    }
    return `${trimmed.slice(0, 10)}…`;
};

const applyCalledState = (cardData, targetName) => {
    if (!cardData?.card) {
        return;
    }
    if (!isCallEligible(cardData?.targetData)) {
        cardData.card.classList.remove('is-yours');
        cardData.card.classList.remove('is-called');
        return;
    }
    const calledBySelf = isTargetCalledBySelf(targetName);
    if (calledBySelf) {
        cardData.card.classList.add('is-yours');
    } else {
        cardData.card.classList.remove('is-yours');
    }
    if (!calledBySelf && isTargetCalled(targetName)) {
        cardData.card.classList.add('is-called');
    } else {
        cardData.card.classList.remove('is-called');
    }
};

const updateCalledBadge = (cardData, targetName) => {
    if (!cardData?.calledBadge) {
        return;
    }
    if (!isCallEligible(cardData?.targetData)) {
        cardData.calledBadge.textContent = 'Called';
        cardData.calledBadge.classList.remove('is-yours');
        cardData.calledBadge.style.display = 'none';
        return;
    }
    if (isTargetCalledBySelf(targetName)) {
        cardData.calledBadge.textContent = 'Yours';
        cardData.calledBadge.classList.add('is-yours');
        cardData.calledBadge.style.display = 'inline-flex';
        return;
    }
    if (isTargetCalled(targetName)) {
        cardData.calledBadge.textContent = formatCallerLabel(
            getTargetCaller(targetName)
        );
        cardData.calledBadge.classList.remove('is-yours');
        cardData.calledBadge.style.display = 'inline-flex';
        return;
    }
    cardData.calledBadge.textContent = 'Called';
    cardData.calledBadge.classList.remove('is-yours');
    cardData.calledBadge.style.display = 'none';
};

const updateCallButtonVisibility = (cardData, targetName) => {
    if (!cardData?.callButton) {
        return;
    }
    const shouldShow =
        isCallEligible(cardData?.targetData) &&
        !isTargetCalled(targetName) &&
        !isTargetCalledBySelf(targetName);
    cardData.callButton.style.display = shouldShow ? 'inline-flex' : 'none';
};

const handleTargetStateTransition = (targetId, targetName, effectiveState) => {
    if (!targetId) {
        logDebug('handleTargetStateTransition skipped: target id missing.', {
            targetName,
            effectiveState,
        });
        return;
    }
    const previousState = targetState.lastKnownStates.get(targetId);
    targetState.lastKnownStates.set(targetId, effectiveState);
    if (previousState !== effectiveState) {
        logDebug('Target state transition tracked.', {
            targetId,
            targetName,
            previousState,
            effectiveState,
        });
    }
    if (previousState === 'Okay' && effectiveState === 'Hospital') {
        if (removeCallsForTargetName(targetName)) {
            logDebug('Removed call entries due to Okay -> Hospital transition.', {
                targetId,
                targetName,
            });
            rebuildCalledState();
            refreshCallStyles();
        }
    }
};

const normalizeMessageKey = (message) => (message ?? '').trim().toLowerCase();

const parseChatMessageParts = (message) => {
    const text = (message ?? '').replace(/\r/g, '').trim();
    if (!text) {
        logDebug('Skipping chat parse: message text is empty after trim.');
        return null;
    }

    const lines = text
        .split('\n')
        .map((line) => line.trim())
        .filter(Boolean);
    if (lines.length === 0) {
        logDebug('Skipping chat parse: no non-empty lines remained.', {
            originalLength: text.length,
        });
        return null;
    }

    const callerMatch = lines[0].match(/^([^:]+):$/);
    if (callerMatch && lines.length >= 2) {
        logDebug('Parsed chat message with explicit caller prefix.', {
            callerName: callerMatch[1].trim(),
            messagePart: lines[1],
            lineCount: lines.length,
        });
        return {
            callerName: callerMatch[1].trim(),
            messagePart: lines[1],
            isSelf: false,
        };
    }

    logDebug('Parsed chat message as self/unprefixed call candidate.', {
        messagePart: lines[0],
        lineCount: lines.length,
    });
    return {
        callerName: '',
        messagePart: lines[0],
        isSelf: true,
    };
};

const extractCallEntry = (message) => {
    if (!message) {
        logDebug('Skipping call extraction: message is empty.');
        return null;
    }

    const messageParts = parseChatMessageParts(message);
    if (!messageParts) {
        logDebug('Skipping call extraction: message parts parsing failed.', {
            rawMessage: message,
        });
        return null;
    }

    const { callerName, messagePart, isSelf } = messageParts;
    const match = messagePart.match(/^([a-z0-9_]+)(?:\s+in\s+\d+)?(?:[.!?,])?$/i);
    if (!match) {
        logDebug('Skipping call extraction: message part did not match fragment pattern.', {
            callerName,
            messagePart,
            isSelf,
        });
        return null;
    }
    const fragment = match[1].toLowerCase();
    const entry = {
        fragment,
        callerName,
        isSelf,
        messageKey: normalizeMessageKey(`${callerName}|${messagePart}`),
    };
    logDebug('Extracted valid call entry from chat message.', entry);
    return entry;
};

const rebuildCalledState = () => {
    const fragments = new Set();
    const exactNames = new Set();
    const calledBy = new Map();
    const calledByExactName = new Map();
    const selfFragments = new Set();
    const selfExactNames = new Set();
    targetState.callEntries.forEach((entry) => {
        exactNames.add(entry.fragment);
        if (entry.isSelf) {
            selfExactNames.add(entry.fragment);
            if (entry.fragment.length >= MIN_CALL_FRAGMENT_LENGTH) {
                selfFragments.add(entry.fragment);
            }
            return;
        }
        if (entry.fragment.length >= MIN_CALL_FRAGMENT_LENGTH) {
            fragments.add(entry.fragment);
        }
        if (entry.callerName) {
            calledByExactName.set(entry.fragment, entry.callerName);
            calledBy.set(entry.fragment, entry.callerName);
        }
    });
    targetState.calledFragments = fragments;
    targetState.calledExactNames = exactNames;
    targetState.calledBy = calledBy;
    targetState.calledByExactName = calledByExactName;
    targetState.selfCalledFragments = selfFragments;
    targetState.selfCalledExactNames = selfExactNames;
    logDebug('Rebuilt called-state indexes.', {
        calledFragments: [...fragments],
        calledExactNames: [...exactNames],
        selfCalledFragments: [...selfFragments],
        selfCalledExactNames: [...selfExactNames],
        calledBy: [...calledBy.entries()],
        calledByExactName: [...calledByExactName.entries()],
    });
};

const refreshCallStyles = () => {
    targetState.cards.forEach((cardData) => {
        const targetName = cardData?.name?.textContent ?? '';
        applyCalledState(cardData, targetName);
        updateCalledBadge(cardData, targetName);
        updateCallButtonVisibility(cardData, targetName);
    });
};

const pruneParsedMessages = (now) => {
    const originalSize = targetState.parsedMessages.size;
    targetState.parsedMessages.forEach((parsedAt, key) => {
        if (now - parsedAt > CALL_FULFILLMENT_TIMEOUT_MS) {
            targetState.parsedMessages.delete(key);
        }
    });
    while (targetState.parsedMessages.size > LAST_N_MESSAGES_TO_CHECK_FOR_DIBS) {
        const oldestKey = targetState.parsedMessages.keys().next().value;
        targetState.parsedMessages.delete(oldestKey);
    }
    if (targetState.parsedMessages.size !== originalSize) {
        logDebug('Pruned parsed message cache.', {
            before: originalSize,
            after: targetState.parsedMessages.size,
            timeoutMs: CALL_FULFILLMENT_TIMEOUT_MS,
            maxTrackedMessages: LAST_N_MESSAGES_TO_CHECK_FOR_DIBS,
        });
    }
};

const pruneCallEntries = (now) => {
    const originalCount = targetState.callEntries.length;
    targetState.callEntries = targetState.callEntries.filter(
        (entry) => now - entry.parsedAt <= CALL_FULFILLMENT_TIMEOUT_MS
    );
    if (targetState.callEntries.length > LAST_N_MESSAGES_TO_CHECK_FOR_DIBS) {
        targetState.callEntries.sort((a, b) => a.parsedAt - b.parsedAt);
        targetState.callEntries.splice(
            0,
            targetState.callEntries.length - LAST_N_MESSAGES_TO_CHECK_FOR_DIBS
        );
    }
    if (targetState.callEntries.length !== originalCount) {
        logDebug('Pruned call fulfillment entries.', {
            before: originalCount,
            after: targetState.callEntries.length,
            timeoutMs: CALL_FULFILLMENT_TIMEOUT_MS,
            maxTrackedMessages: LAST_N_MESSAGES_TO_CHECK_FOR_DIBS,
        });
    }
};

const removeCallsForTargetName = (targetName) => {
    if (!targetName) {
        logDebug('removeCallsForTargetName skipped: missing target name.');
        return false;
    }
    const lowerName = targetName.toLowerCase();
    const originalLength = targetState.callEntries.length;
    targetState.callEntries = targetState.callEntries.filter((entry) => {
        if (lowerName === entry.fragment) {
            return false;
        }
        if (entry.fragment.length < MIN_CALL_FRAGMENT_LENGTH) {
            return true;
        }
        return !lowerName.startsWith(entry.fragment);
    });
    const removed = targetState.callEntries.length !== originalLength;
    if (removed) {
        logDebug('Removed call entries for target fulfillment target.', {
            targetName,
            removedCount: originalLength - targetState.callEntries.length,
        });
    }
    return removed;
};

const updateCalledTargets = (messages) => {
    const now = Date.now();
    logDebug('Starting called-target update cycle.', {
        incomingMessageCount: (messages ?? []).length,
        existingCallEntries: targetState.callEntries.length,
        parsedMessageCacheSize: targetState.parsedMessages.size,
    });
    (messages ?? []).forEach((message) => {
        const entry = extractCallEntry(message);
        if (!entry) {
            return;
        }
        if (targetState.parsedMessages.has(entry.messageKey)) {
            logDebug('Skipping duplicate call entry based on message key.', {
                messageKey: entry.messageKey,
                fragment: entry.fragment,
            });
            return;
        }
        targetState.parsedMessages.set(entry.messageKey, now);
        const hadExistingFragment = targetState.callEntries.some(
            (existing) => existing.fragment === entry.fragment
        );
        targetState.callEntries = targetState.callEntries.filter(
            (existing) => existing.fragment !== entry.fragment
        );
        targetState.callEntries.push({ ...entry, parsedAt: now });
        logDebug('Registered/updated call fulfillment entry.', {
            fragment: entry.fragment,
            callerName: entry.callerName,
            isSelf: entry.isSelf,
            replacedExistingFragment: hadExistingFragment,
        });
    });

    pruneParsedMessages(now);
    pruneCallEntries(now);
    rebuildCalledState();
    refreshCallStyles();
    logDebug('Completed called-target update cycle.', {
        totalCallEntries: targetState.callEntries.length,
        calledFragmentsCount: targetState.calledFragments.size,
        selfCalledFragmentsCount: targetState.selfCalledFragments.size,
    });
};

const ensureTargetLayout = () => {
    const content = document.getElementById(CONTENT_ELEMENT_ID);
    if (!content) {
        return null;
    }

    ensureTargetStyles();

    if (!targetState.wrapper) {
        content.textContent = '';
        targetState.wrapper = document.createElement('div');
        targetState.wrapper.className = 'war-targets-wrapper';

        targetState.message = document.createElement('div');
        targetState.message.className = 'war-targets-message';

        targetState.settingsPanel = document.createElement('div');
        targetState.settingsPanel.className = 'war-targets-settings';

        const minLabel = document.createElement('label');
        minLabel.textContent = 'min FF';
        targetState.minFairFightInput = document.createElement('input');
        targetState.minFairFightInput.type = 'number';
        targetState.minFairFightInput.step = '0.1';
        targetState.minFairFightInput.min = '0';
        minLabel.appendChild(targetState.minFairFightInput);

        const maxLabel = document.createElement('label');
        maxLabel.textContent = 'max FF';
        targetState.maxFairFightInput = document.createElement('input');
        targetState.maxFairFightInput.type = 'number';
        targetState.maxFairFightInput.step = '0.1';
        targetState.maxFairFightInput.min = '0';
        maxLabel.appendChild(targetState.maxFairFightInput);

        targetState.settingsPanel.appendChild(minLabel);
        targetState.settingsPanel.appendChild(maxLabel);
        const onlineFirstLabel = document.createElement('label');
        onlineFirstLabel.textContent = 'onlines first';
        targetState.onlineFirstInput = document.createElement('input');
        targetState.onlineFirstInput.type = 'checkbox';
        targetState.onlineFirstInput.checked = targetState.onlineFirst;
        onlineFirstLabel.appendChild(targetState.onlineFirstInput);
        targetState.settingsPanel.appendChild(onlineFirstLabel);
        targetState.minFairFightInput.addEventListener('input', applyFairFightFilter);
        targetState.maxFairFightInput.addEventListener('input', applyFairFightFilter);
        targetState.onlineFirstInput.addEventListener('input', applyOnlineFirstSetting);
        updateFairFightInputs();

        targetState.grid = document.createElement('div');
        targetState.grid.className = 'war-targets-grid';

        targetState.wrapper.appendChild(targetState.settingsPanel);
        targetState.wrapper.appendChild(targetState.message);
        targetState.wrapper.appendChild(targetState.grid);
        content.appendChild(targetState.wrapper);
    }

    return targetState.wrapper;
};

const renderTargetGrid = (targets) => {
    if (!ensureTargetLayout()) {
        logWarn('Target layout is unavailable; cannot render target grid.');
        return;
    }

    targetState.latestTargets = Array.isArray(targets) ? targets : [];
    const visibleTargets = targetState.latestTargets.filter(
        (target) => {
            const fairFight = Number(target?.fair_fight);
            return (
                Number.isFinite(fairFight) &&
                fairFight >= targetState.minFairFight &&
                fairFight <= targetState.maxFairFight &&
                !isAbroadHospitalTarget(target)
            );
        }
    );
    const sortedTargets = visibleTargets
        .map((target, index) => ({ target, index }))
        .sort((first, second) => {
            if (targetState.onlineFirst) {
                const firstOnline =
                    (first.target?.availability_status ?? '').toLowerCase() ===
                    'online';
                const secondOnline =
                    (second.target?.availability_status ?? '').toLowerCase() ===
                    'online';
                if (firstOnline !== secondOnline) {
                    return firstOnline ? -1 : 1;
                }
            }

            const firstState = getEffectiveState(first.target);
            const secondState = getEffectiveState(second.target);
            const firstGroup =
                firstState === 'Okay' ? 0 : firstState === 'Hospital' ? 1 : 2;
            const secondGroup =
                secondState === 'Okay' ? 0 : secondState === 'Hospital' ? 1 : 2;

            if (firstGroup !== secondGroup) {
                return firstGroup - secondGroup;
            }

            if (firstGroup === 0) {
                const firstFf = Number(first.target?.fair_fight);
                const secondFf = Number(second.target?.fair_fight);
                const firstValue = Number.isFinite(firstFf)
                    ? firstFf
                    : Number.NEGATIVE_INFINITY;
                const secondValue = Number.isFinite(secondFf)
                    ? secondFf
                    : Number.NEGATIVE_INFINITY;
                if (firstValue !== secondValue) {
                    return secondValue - firstValue;
                }
            }

            if (firstGroup === 1) {
                const firstSeconds = getHospitalRemainingSeconds(
                    first.target?.status?.until
                );
                const secondSeconds = getHospitalRemainingSeconds(
                    second.target?.status?.until
                );
                const firstValue =
                    firstSeconds == null ? Number.POSITIVE_INFINITY : firstSeconds;
                const secondValue =
                    secondSeconds == null ? Number.POSITIVE_INFINITY : secondSeconds;
                if (firstValue !== secondValue) {
                    return firstValue - secondValue;
                }
            }

            return first.index - second.index;
        })
        .map(({ target }) => target);

    if (visibleTargets.length === 0) {
        targetState.message.textContent = 'No targets found.';
        targetState.message.style.display = 'block';
        targetState.grid.style.display = 'none';
        return;
    }

    targetState.message.textContent = '';
    targetState.message.style.display = 'none';
    targetState.grid.style.display = 'grid';

    const seenIds = new Set();
    sortedTargets.forEach((target) => {
        const targetId = target?.id ?? '';
        if (!targetId) {
            logWarn('Skipping target without id.', { target });
            return;
        }
        seenIds.add(String(targetId));
        let cardData = targetState.cards.get(String(targetId));
        if (!cardData) {
            cardData = renderTargetCard(target);
            targetState.cards.set(String(targetId), cardData);
        } else {
            updateTargetCard(target, cardData);
        }

        // Keep DOM order aligned with sorted target order on every refresh.
        targetState.grid.appendChild(cardData.card);
    });

    targetState.cards.forEach((cardData, id) => {
        if (!seenIds.has(id)) {
            cardData.card.remove();
            targetState.cards.delete(id);
        }
    });
};

const setContentMessage = (message) => {
    if (!ensureTargetLayout()) {
        logWarn('Target layout is unavailable; cannot set content message.', {
            message,
        });
        return;
    }
    targetState.message.textContent = message;
    targetState.message.style.display = 'block';
    targetState.grid.style.display = 'none';
};

const getStoredApiKey = () => {
    const storedKey = safeGetValue(FFSCOUTER_API_KEY_STORAGE_KEY, '');
    if (typeof storedKey === 'string') {
        return storedKey.trim();
    }

    return '';
};

const setStoredApiKey = (key) => {
    safeSetValue(FFSCOUTER_API_KEY_STORAGE_KEY, key);
};

const renderApiKeyMessage = (message, initialValue = '') => {
    if (!ensureTargetLayout()) {
        return;
    }

    targetState.message.textContent = '';

    const textElement = document.createElement('div');
    textElement.textContent = message;

    const form = document.createElement('div');
    form.className = 'war-targets-api-key-form';

    const keyInput = document.createElement('input');
    keyInput.className = 'war-targets-api-key-input';
    keyInput.type = 'text';
    keyInput.maxLength = FFSCOUTER_KEY_LENGTH;
    keyInput.placeholder = 'Enter your FFScouter API key';
    keyInput.value = initialValue;

    const saveButton = document.createElement('button');
    saveButton.className = 'war-targets-api-key-button';
    saveButton.type = 'button';
    saveButton.textContent = 'Save';

    const submitApiKey = () => {
        const key = keyInput.value.trim();
        setStoredApiKey(key);
        verifyApiKey(key);
    };

    saveButton.addEventListener('click', submitApiKey);
    keyInput.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') {
            event.preventDefault();
            submitApiKey();
        }
    });

    form.appendChild(keyInput);
    form.appendChild(saveButton);
    targetState.message.appendChild(textElement);
    targetState.message.appendChild(form);

    targetState.message.style.display = 'block';
    targetState.grid.style.display = 'none';
};

const requestJson = (url) => {
    const requestFn = isFunction(globalThis.GM_xmlhttpRequest)
        ? globalThis.GM_xmlhttpRequest
        : isFunction(getModernGmApi()?.xmlHttpRequest)
          ? getModernGmApi().xmlHttpRequest
          : null;

    if (requestFn) {
        return new Promise((resolve, reject) => {
            requestFn({
                method: 'GET',
                url,
                timeout: REQUEST_TIMEOUT_MS,
                onload: (response) => {
                    if (response.status < 200 || response.status >= 300) {
                        logError('HTTP request failed.', {
                            url,
                            status: response.status,
                            responseText: response.responseText,
                        });
                        reject(new Error(`Request failed with status ${response.status}`));
                        return;
                    }

                    try {
                        resolve(JSON.parse(response.responseText));
                    } catch (error) {
                        logError('Failed to parse JSON response.', {
                            url,
                            responseText: response.responseText,
                            error,
                        });
                        reject(error);
                    }
                },
                onerror: (error) => {
                    logError('Network request failed.', { url, error });
                    reject(new Error('Network request failed.'));
                },
                ontimeout: () => {
                    logError('Network request timed out.', { url, timeoutMs: REQUEST_TIMEOUT_MS });
                    reject(new Error('Request timed out.'));
                },
            });
        });
    }

    const tornPdaHttpGet = getTornPdaHttpGet();
    if (isFunction(tornPdaHttpGet)) {
        return Promise.resolve(tornPdaHttpGet(url))
            .then((response) => {
                const text = typeof response === 'string' ? response : response?.responseText;
                if (!text) {
                    logError('TornPDA request returned an empty response.', { url, response });
                    throw new Error('TornPDA request returned an empty response.');
                }
                return JSON.parse(text);
            })
            .catch((error) => {
                logError('TornPDA request failed.', { url, error });
                throw error;
            });
    }

    return fetch(url, { method: 'GET' })
        .then((response) => {
            if (!response.ok) {
                logError('Fetch request failed.', { url, status: response.status });
                throw new Error(`Request failed with status ${response.status}`);
            }
            return response.json();
        })
        .catch((error) => {
            logError('Fetch request failed unexpectedly.', { url, error });
            throw error;
        });
};

const fetchFactionInfo = async (key) => {
    const data = await requestJson(
        `https://api.torn.com/v2/faction?key=${encodeURIComponent(key)}`
    );
    const id = data?.basic?.id;
    const name = data?.basic?.name;

    if (!id || !name) {
        logError('Faction info response is missing required fields.', { data });
        throw new Error('Unable to read faction information.');
    }

    return { id: String(id), name };
};

const fetchEnemyFaction = async (key, currentFactionId) => {
    if (!currentFactionId) {
        logError('Missing current faction id while fetching enemy faction.');
        throw new Error('Missing faction id.');
    }

    const data = await requestJson(
        `https://api.torn.com/v2/faction/rankedwars?key=${encodeURIComponent(
            key
        )}&limit=1`
    );
    const rankedWarsRaw = data?.rankedwars ?? data;
    const rankedWars = Array.isArray(rankedWarsRaw)
        ? rankedWarsRaw
        : Object.values(rankedWarsRaw ?? {});
    const nowMs = Date.now();
    const candidates = rankedWars
        .filter((war) => !isCompletedRankedWar(war))
        .map((war) => {
            const factions = war?.factions ?? [];
            const enemyFaction = factions.find(
                (faction) => String(faction?.id) !== String(currentFactionId)
            );
            const startAtMs = toTimestampMs(war?.start);
            const isUpcoming = Number.isFinite(startAtMs) && startAtMs > nowMs;

            return { enemyFaction, startAtMs, isUpcoming };
        })
        .filter((entry) => Boolean(entry.enemyFaction?.id));

    if (candidates.length === 0) {
        logInfo('No active ranked war found for current faction.', {
            currentFactionId,
            rankedWarCount: rankedWars.length,
        });
        return null;
    }

    const ongoingCandidates = candidates.filter((entry) => !entry.isUpcoming);
    const upcomingCandidates = candidates.filter((entry) => entry.isUpcoming);
    ongoingCandidates.sort((first, second) => {
        const firstStart = first.startAtMs ?? -Infinity;
        const secondStart = second.startAtMs ?? -Infinity;
        return secondStart - firstStart;
    });
    upcomingCandidates.sort((first, second) => {
        const firstStart = first.startAtMs ?? Infinity;
        const secondStart = second.startAtMs ?? Infinity;
        return firstStart - secondStart;
    });
    const selected = ongoingCandidates[0] ?? upcomingCandidates[0] ?? candidates[0];
    const enemyFaction = selected?.enemyFaction;

    if (!enemyFaction?.id) {
        logError('Unable to resolve enemy faction from ranked wars response.', {
            currentFactionId,
            rankedWars,
            selected,
        });
        throw new Error('Unable to resolve enemy faction.');
    }

    return {
        id: String(enemyFaction.id),
        name: enemyFaction?.name ?? '',
        warStarted: !selected?.isUpcoming,
    };
};

const fetchEnemyTargets = async (key, enemyId, { useScouter = true } = {}) => {
    if (!enemyId) {
        logWarn('Enemy faction id missing; skipping target fetch.');
        return [];
    }

    const membersData = await requestJson(
        `https://api.torn.com/v2/faction/${encodeURIComponent(
            enemyId
        )}/members?key=${encodeURIComponent(key)}`
    );
    const rawMembers = Array.isArray(membersData?.members)
        ? membersData.members
        : Object.values(membersData?.members ?? {});
    const memberIds = rawMembers
        .map((member) => member?.id)
        .filter((id) => Boolean(id));

    if (memberIds.length === 0) {
        logWarn('No enemy members found.', { enemyId });
        return [];
    }

    if (useScouter && memberIds.length > 0 && statsCache.size === 0) {
        const statsData = await requestJson(
            `https://ffscouter.com/api/v1/get-stats?key=${encodeURIComponent(
                key
            )}&targets=${memberIds.join(',')}`
        );
        (Array.isArray(statsData) ? statsData : []).forEach((stat) => {
            if (stat?.player_id) {
                statsCache.set(stat.player_id, stat);
            }
        });
        if (statsCache.size === 0) {
            logWarn('FFScouter returned no stats for requested member ids.', {
                enemyId,
                memberCount: memberIds.length,
            });
        }
    }

    return rawMembers
        .map((member) => {
            const stats = statsCache.get(member?.id);
            if (!stats || stats.fair_fight == null) {
                return null;
            }

            return {
                ...member,
                availability_status: getAvailabilityStatus(member),
                fair_fight: stats.fair_fight,
                bs_estimate_human: stats.bs_estimate_human,
            };
        })
        .filter(
            (member) =>
                Boolean(member) &&
                !isFederalTarget(member) &&
                !isTravelingTarget(member) &&
                !isAbroadTarget(member)
        );
};

const loadTargets = async (key, options = {}) => {
    const { showLoading = true, ...requestOptions } = options;
    setNoActiveWarHeaderState(false);
    if (showLoading) {
        setContentMessage('Loading targets...');
    }
    const factionInfo = await fetchFactionInfo(key);
    const enemyInfo = await fetchEnemyFaction(key, factionInfo?.id);
    if (!enemyInfo?.id) {
        headerState.warStarted = false;
        headerState.lastUpdatedAt = null;
        setNoActiveWarHeaderState(true);
        return;
    }
    headerState.warStarted = enemyInfo.warStarted === true;
    setNoActiveWarHeaderState(false);
    const targets = await fetchEnemyTargets(key, enemyInfo?.id, requestOptions);
    headerState.lastUpdatedAt = Date.now();
    renderTargetGrid(targets);
};

const getTargetRefreshIntervalMs = () =>
    headerState.warStarted
        ? TARGET_REFRESH_INTERVAL_IN_WAR_MS
        : TARGET_REFRESH_INTERVAL_NOT_IN_WAR_MS;

const updateLastUpdatedText = () => {
    const header = document.getElementById(HEADER_ELEMENT_ID);
    const lastUpdated = header?.querySelector('.war-targets-last-updated');
    if (!lastUpdated) {
        return;
    }

    if (!headerState.lastUpdatedAt) {
        lastUpdated.textContent = 'Last updated: --';
        return;
    }

    const seconds = Math.max(
        0,
        Math.floor((Date.now() - headerState.lastUpdatedAt) / 1000)
    );
    lastUpdated.textContent = `Last updated: ${seconds} seconds ago`;
};

const startLastUpdatedTimer = () => {
    if (headerState.lastUpdatedTimer) {
        clearInterval(headerState.lastUpdatedTimer);
    }
    headerState.lastUpdatedTimer = setInterval(
        updateLastUpdatedText,
        LAST_UPDATED_INTERVAL_MS
    );
    updateLastUpdatedText();
};

const startAutoRefresh = (key) => {
    if (headerState.refreshTimer) {
        clearTimeout(headerState.refreshTimer);
    }

    const runRefresh = () => {
        loadTargets(key, { useScouter: false, showLoading: false })
            .catch((error) => {
                logError('Auto-refresh failed.', { error });
                setContentMessage('Unable to refresh targets.');
            })
            .finally(() => {
                headerState.refreshTimer = setTimeout(
                    runRefresh,
                    getTargetRefreshIntervalMs()
                );
            });
    };

    headerState.refreshTimer = setTimeout(runRefresh, getTargetRefreshIntervalMs());
};

const verifyApiKey = (key) => {
    if (!key || key.length !== FFSCOUTER_KEY_LENGTH) {
        renderApiKeyMessage(
            'Invalid API key! Please use the same you use for FFScouter.',
            key
        );
        return;
    }

    setContentMessage('Validating API key...');

    requestJson(
        `https://ffscouter.com/api/v1/check-key?key=${encodeURIComponent(key)}`
    )
        .then((data) => {
            if (!data?.is_registered) {
                throw new Error('Invalid API key.');
            }
            setStoredApiKey(key);
            return loadTargets(key).then(() => {
                startAutoRefresh(key);
                startLastUpdatedTimer();
            });
        })
        .catch((error) => {
            logError('API key validation failed.', { key, error });
            renderApiKeyMessage(
                'Unable to validate API key. Please confirm your key and try again.',
                key
            );
        });
};

const getTargetContainer = () => {
    const factionMain = document.getElementById('faction-main');
    if (!factionMain) {
        logWarn('Faction main container not found yet.');
    }
    return factionMain?.children?.[0]?.children?.[0]?.children?.[0] ?? null;
};

function renderNewElements() {
    const targetDiv = getTargetContainer();
    if (!targetDiv) {
        logWarn('Target container not available; delaying initialization.');
        return false;
    }

    if (document.getElementById(HEADER_ELEMENT_ID)) {
        return true;
    }

    // Add new divs
    // Header
    const headerDiv = document.createElement('div');
    const headerContent = document.createElement('div');
    headerContent.className = 'war-targets-header-content';

    const headerTitle = document.createElement('span');
    headerTitle.className = HEADER_TITLE_CLASS;
    headerTitle.textContent = getHeaderTitle();

    const lastUpdated = document.createElement('span');
    lastUpdated.className = 'war-targets-last-updated';
    lastUpdated.textContent = 'Last updated: --';

    headerContent.appendChild(headerTitle);
    headerContent.appendChild(lastUpdated);
    headerDiv.appendChild(headerContent);
    headerDiv.className =
        'title-black m-top10 tablet active top-round war-targets-header';
    headerDiv.id = HEADER_ELEMENT_ID;
    headerDiv.setAttribute('aria-expanded', 'true');
    headerDiv.addEventListener('click', (event) => {
        // Ignore synthetic clicks that can be dispatched by host-page accordion scripts.
        if (!event.isTrusted) {
            return;
        }
        toggleCollapsedState();
    });
    const fifthChild = targetDiv.children[4];
    targetDiv.insertBefore(headerDiv, fifthChild);
    // Content
    const contentDiv = document.createElement('div');
    contentDiv.textContent = 'Checking API key...';
    contentDiv.className =
        'cont-gray10 bottom-round editor-content announcement unreset scrollbar-bright war-targets-content';
    contentDiv.id = 'war-tagets-content';
    const sixthChild = targetDiv.children[5];
    targetDiv.insertBefore(contentDiv, sixthChild);
    setNoActiveWarHeaderState(false);

    return true;
}

(async function() {
    'use strict';

    waitForChatRoot();

    const initialize = () => {
        if (!renderNewElements()) {
            logWarn('Render preconditions not met; retrying initialization in 500ms.');
            setTimeout(initialize, 500);
            return;
        }
        const storedApiKey = getStoredApiKey();
        if (!storedApiKey) {
            headerState.isCollapsed = false;
            setNoActiveWarHeaderState(headerState.noActiveWar);
        }
        verifyApiKey(storedApiKey);
    };

    setTimeout(initialize, 1000);
})();