var orchestratorApiBaseUrl = '';
var orchestratorApiKey = '';
var publisherId = '';
var extensionId = '';
var baseAPIUrl = '';
let userLookup = {};
let uiTimerStarted = false;
let runningTimers = {};
let refreshInterval;
let currentStatusFilter = 'all';
let currentViewMode = 'table';
let currentSearchTerm = '';
let currentSortField = 'Queued';
let currentSortDirection = 'desc';
let latestDeveloperExecutions = [];
let hasDeveloperAgentLicense = false;
let hasPrototypeAgentLicense = false;

// Org "Release and task scope level": Project scope confines the queue dialog to this project.
// Defaults to project scope because the API does the same for an unconfigured org, and because a
// failed read must not silently widen the repository list to the whole organization.
let isProjectScopedOrg = true;
let currentProjectContext = { id: '', name: '' };

// Signature of the execution list as it was last drawn — see buildExecutionsSignature.
let lastExecutionsSignature = null;

// Which prototype version is currently the approved one for this work item, derived from the
// spec file attached to it (no server-side state): "Task_1089_v16.md" => version 16.
let selectedPrototype = { version: null, fileName: null };
let prototypeLicenseWarningLogged = false;

// Friendly labels for the v2 Developer Agent steps (keyed by the raw step name the backend
// returns in `currentStep`, with any ":repoName" suffix already stripped server-side).
const STEP_SEQUENCE = [
    { key: "RequirementRefinement", label: "Refining requirements" },
    { key: "RepoDiscovery", label: "Discovering repositories" },
    { key: "CodeAnalysis", label: "Analyzing code" },
    { key: "SolutionPlanning", label: "Planning solution" },
    { key: "CodeWriter", label: "Writing code" },
    { key: "CodeVerification", label: "Verifying code" },
    { key: "PersistToBlob", label: "Saving changes" },
    { key: "GitOperations", label: "Creating pull request" }
];

const STEP_LABELS = STEP_SEQUENCE.reduce((acc, step) => {
    acc[step.key] = step.label;
    return acc;
}, {});

const DEVELOPER_AGENT_TABLE_HTML = `
    <div class="cr-grid-actions" id="daGridActions">
        <span class="cr-view-switch" id="daViewSwitch">
            <button type="button" class="cr-view-btn active" data-view="table" title="Table view"><i class="bi bi-table"></i><span>Table</span></button>
            <button type="button" class="cr-view-btn" data-view="cards" title="Cards view"><i class="bi bi-grid-3x3-gap"></i><span>Cards</span></button>
        </span>
        <div class="cr-search-wrap">
            <i class="bi bi-search"></i>
            <input id="daSearchInput" class="cr-input" type="text" placeholder="Search (Queued By, Project, Status, Output)" />
        </div>
        <button type="button" id="daResetGridBtn" class="cr-mini-btn" title="Reset filters">
            <i class="bi bi-arrow-counterclockwise"></i>
            <span>Reset</span>
        </button>
    </div>
    <div class="cr-toolbar">
        <div class="cr-filter-chips" id="daFilterChips">
            <button type="button" class="cr-filter-chip active" data-filter="all">All</button>
            <button type="button" class="cr-filter-chip" data-filter="running">Running</button>
            <button type="button" class="cr-filter-chip" data-filter="completed">Completed</button>
            <button type="button" class="cr-filter-chip" data-filter="failed">Failed</button>
        </div>
        <div id="DeveloperAgentSummary" class="cr-summary-inline">
            <div class="cr-summary-item total"><span class="label">Total</span><span id="daSummaryTotal" class="value">0</span></div>
            <div class="cr-summary-item running"><span class="label">Running</span><span id="daSummaryRunning" class="value">0</span></div>
            <div class="cr-summary-item completed"><span class="label">Completed</span><span id="daSummaryCompleted" class="value">0</span></div>
            <div class="cr-summary-item failed"><span class="label">Failed</span><span id="daSummaryFailed" class="value">0</span></div>
            <div class="cr-summary-item success-rate">
                <span class="label">Success Rate</span>
                <span id="daSummarySuccessRateText" class="value">0%</span>
                <div class="cr-success-track compact">
                    <div id="daSummarySuccessRateBar" class="cr-success-bar" style="width:0%"></div>
                </div>
            </div>
        </div>
    </div>
    <div id="DeveloperAgentCardView" class="cr-card-grid cr-theme-azure" style="display:none;"></div>
    <div class="cr-table-scroll" id="DeveloperAgentTableScroll">
        <table id='DeveloperAgentExecutionTable' class='cr-table' style='width:100%'>
            <thead>
                <tr>
                    <th data-sort="QueuedBy"><i class="bi bi-person"></i> Queued By <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Project">Project <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Queued">Queued <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Started">Started <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Completed">Completed <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Duration">Duration <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Status">Status <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                    <th data-sort="Output">Output <span class="sort-icon"><i class="bi bi-caret-down-fill"></i></span></th>
                </tr>
            </thead>
            <tbody id='DeveloperAgentExecutionTableBody'></tbody>
        </table>
    </div>`;

function parseUTCDate(dateStr) {
    if (!dateStr) return null;
    return new Date(dateStr.endsWith('Z') ? dateStr : dateStr + 'Z');
}

function formatDateTimeLocal(dateStr) {
    const date = parseUTCDate(dateStr);
    if (!date) return "-";

    const dateStrFormatted = date.toLocaleDateString("en-US", { month: 'short', day: 'numeric' });
    const timeStrFormatted = date.toLocaleTimeString("en-US", { hour: 'numeric', minute: '2-digit', hour12: true });
    return `<div class="datetime-cell"><div class="datetime-date"><i class="bi bi-calendar3"></i> ${dateStrFormatted}</div><div class="datetime-time"><i class="bi bi-clock"></i> ${timeStrFormatted}</div></div>`;
}

// Turns bare http(s) URLs inside ALREADY-ESCAPED text into clickable links (e.g. the
// "Deployed site: https://…" line the developer agent puts in its completion details).
function linkifyUrls(escapedHtml) {
    return escapedHtml.replace(/https?:\/\/[^\s<>"']+/g, (url) => {
        const trimmed = url.replace(/[.,;:!?)]+$/, '');
        const trailing = url.slice(trimmed.length);
        return `<a href="${trimmed}" target="_blank" rel="noopener noreferrer">${trimmed}</a>${trailing}`;
    });
}

// ── Prototype selection ────────────────────────────────────────────────────────
// A prototype run reports "Deployed site: https://…/prototype/{org}/{project}/{workItem}/v2/".
// That URL is parsed rather than the row's own fields because it is the exact path the agent
// deployed to.
function parsePrototypeDeployment(output) {
    if (!output) return null;

    const match = /Deployed site:\s*(https?:\/\/[^\s<>"']+)/i.exec(output);
    if (!match) return null;

    const segments = match[1].split('?')[0].split('#')[0].split('/').filter(Boolean);

    const prototypeIndex = segments.lastIndexOf('prototype');
    if (prototypeIndex >= 0 && segments.length - prototypeIndex === 5) {
        const [organizationId, projectId, workItemId, version] = segments.slice(prototypeIndex + 1);
        if (/^v\d+$/.test(version)) {
            return { projectId, workItemId, version, deploymentId: version, isViewable: true };
        }
    }

    // Legacy "…/1089/" or "…/1089.15/" runs: the sites are gone, but their snapshots stay selectable.
    const last = segments[segments.length - 1];
    if (/^\d+(\.\d+)?$/.test(last || '')) {
        return { projectId: null, workItemId: null, version: null, deploymentId: last, isViewable: false };
    }

    return null;
}

// Current ids are "v2". Legacy ones: "1089" was v1, "1089.1" v2, "1089.2" v3.
function prototypeVersionFromDeploymentId(deploymentId) {
    if (!deploymentId) return null;

    const modern = /^v(\d+)$/i.exec(deploymentId);
    if (modern) return parseInt(modern[1], 10);

    const dot = deploymentId.indexOf('.');
    if (dot < 0) return 1;
    const suffix = parseInt(deploymentId.slice(dot + 1), 10);
    return Number.isInteger(suffix) && suffix > 0 ? suffix + 1 : 1;
}

// The URL needs the session handoff to be followed, so the View button replaces it.
function stripDeployedSiteLine(output, deployment) {
    if (!output || !deployment?.isViewable) return output;

    const stripped = output.replace(/^.*Deployed site:.*$/mi, '').replace(/\n{2,}/g, '\n').trim();
    return stripped.length > 0 ? stripped : '-';
}

// Not gated on the Prototype Agent licence the way Select is: viewing is not a licensed action.
function buildPrototypeViewButton(deployment) {
    if (!deployment?.isViewable) return '';

    return `
        <button type="button"
                class="proto-view-btn"
                data-project-id="${IndexLibrary.escapeHtml(deployment.projectId)}"
                data-work-item-id="${IndexLibrary.escapeHtml(deployment.workItemId)}"
                data-version="${IndexLibrary.escapeHtml(deployment.version)}"
                data-bs-toggle="tooltip"
                data-bs-title="Opens this prototype in a new tab. Only people who can read this work item can open it."
                data-bs-placement="top">
            <i class="bi bi-box-arrow-up-right"></i>
            <span>View ${IndexLibrary.escapeHtml(deployment.version)}</span>
        </button>`;
}

async function openPrototype(button, tab) {
    const projectId = button.data('project-id');
    const workItemId = button.data('work-item-id');
    const version = button.data('version');

    try {
        const response = await IndexLibrary.PostRequest({
            baseAPIUrl: baseAPIUrl,
            endpoint: '/api/prototype/session',
            body: {
                projectId: `${projectId}`,
                workItemId: `${workItemId}`,
                version: `${version}`
            }
        });

        if (!response.ok) {
            tab?.close();
            let message = `Could not open prototype ${version}.`;
            if (response.status === 403) {
                message = 'You do not have access to this work item, so its prototype cannot be opened.';
            } else if (response.status === 404) {
                message = `Prototype ${version} is no longer available.`;
            }
            await IndexLibrary.ShowToast('error', message);
            return;
        }

        const payload = await response.json();
        const url = `${(baseAPIUrl || '').replace(/\/+$/, '')}${payload.path}`;

        if (tab) {
            // The prototype is generated code; don't hand it a reference back to this page.
            try { tab.opener = null; } catch { /* not settable in every browser */ }
            tab.location = url;
        } else {
            window.open(url, '_blank', 'noopener');
        }
    } catch (error) {
        tab?.close();
        await IndexLibrary.ShowToast('error', 'Could not reach the service to open this prototype.');
    }
}

// The attached spec file IS the selection state — no server-side flag to drift out of sync.
// Named "<Type>_<workItemId>_v<n>.md" by the agent; a user's own .md never matches.
async function refreshSelectedPrototype() {
    try {
        const workItemId = await IndexLibrary.CurrentWorkItem();
        const workItem = await IndexLibrary.GetWorkItemWithRelations(Number(workItemId));
        const pattern = new RegExp(`^[A-Za-z0-9]+_${workItemId}_v(\\d+)\\.md$`, 'i');

        for (const relation of (workItem?.relations || [])) {
            if ((relation.rel || '').toLowerCase() !== 'attachedfile') continue;
            const name = relation.attributes?.name || '';
            const match = pattern.exec(name);
            if (match) {
                selectedPrototype = { version: parseInt(match[1], 10), fileName: name };
                return;
            }
        }
        selectedPrototype = { version: null, fileName: null };
    } catch (error) {
        // Leave the last known state rather than wrongly showing everything as unselected.
        console.warn('Could not read the work item attachments for prototype selection state.', error);
    }
}

function buildPrototypeSelectButton(element, deployment) {
    if ((element.status || '').toLowerCase() !== 'completed') return '';

    const deploymentId = deployment?.deploymentId;
    if (!deploymentId) return '';

    // Hiding the button on a row that plainly shows a prototype URL looks like a bug, so say
    // why once: paUserType was introduced without a backfill, and users predating it have none.
    if (!hasPrototypeAgentLicense) {
        if (!prototypeLicenseWarningLogged) {
            prototypeLicenseWarningLogged = true;
            console.warn('Prototype "Select" buttons are hidden: this user has no Prototype Agent license (paUserType is not set for them under Licensed Users).');
        }
        return '';
    }

    const version = prototypeVersionFromDeploymentId(deploymentId);
    const isSelected = selectedPrototype.version === version;

    let tooltip;
    if (isSelected) {
        tooltip = `Selected — ${selectedPrototype.fileName || 'its specification'} is attached to this work item and will be used by the AI Developer agent.`;
    } else if (selectedPrototype.version) {
        tooltip = `Select v${version} instead — replaces ${selectedPrototype.fileName} as the AI Developer agent's input.`;
    } else {
        tooltip = 'Attaches its specification to this work item as input for the AI Developer agent.';
    }

    return `
        <button type="button"
                class="proto-select-btn${isSelected ? ' is-selected' : ''}"
                data-deployment-id="${IndexLibrary.escapeHtml(deploymentId)}"
                data-version="${version}"
                data-project-name="${IndexLibrary.escapeHtml(element.projectName || '')}"
                data-project-id="${IndexLibrary.escapeHtml(element.projectId || '')}"
                aria-disabled="${isSelected}"
                data-bs-toggle="tooltip"
                data-bs-title="${IndexLibrary.escapeHtml(tooltip)}"
                data-bs-placement="top">
            <i class="bi ${isSelected ? 'bi-check-circle-fill' : 'bi-check2-square'}"></i>
            <span>${isSelected ? `Selected v${version}` : `Select v${version}`}</span>
        </button>`;
}

async function selectPrototype(button) {
    const deploymentId = button.data('deployment-id');
    const version = button.data('version');
    const projectName = button.data('project-name');
    const projectId = button.data('project-id');

    const originalHtml = button.html();
    button.addClass('is-busy').attr('aria-disabled', 'true')
        .html('<i class="bi bi-hourglass-split"></i><span>Selecting…</span>');
    // The tooltip would otherwise hang around over a button that has already changed.
    bootstrap.Tooltip.getInstance(button[0])?.hide();

    try {
        const response = await IndexLibrary.PostRequest({
            baseAPIUrl: orchestratorApiBaseUrl,
            endpoint: '/api/prototype/select',
            xApiKey: orchestratorApiKey,
            body: {
                organizationId: IndexLibrary.GetAccountId(),
                organizationName: await IndexLibrary.GetAccountNameAsync(),
                projectId: projectId || '',
                projectName: projectName || '',
                workItemId: `${await IndexLibrary.CurrentWorkItem()}`,
                deploymentId: `${deploymentId}`,
                createdBy: IndexLibrary.GetUserEmail()
            }
        });

        let payload = {};
        try { payload = await response.json(); } catch { /* non-JSON error body */ }

        if (!response.ok) {
            button.removeClass('is-busy').attr('aria-disabled', 'false').html(originalHtml);
            await IndexLibrary.ShowToast('error', payload.message || `Could not select prototype v${version}.`);
            return;
        }

        selectedPrototype = { version: payload.version ?? version, fileName: payload.fileName || null };
        const replaced = Array.isArray(payload.removed) && payload.removed.length > 0
            ? ` It replaced ${payload.removed.join(', ')}.`
            : '';
        await IndexLibrary.ShowToast('success', `Prototype v${selectedPrototype.version} selected. ${payload.fileName || 'The specification'} is attached to this work item.${replaced}`);
    } catch (error) {
        button.removeClass('is-busy').attr('aria-disabled', 'false').html(originalHtml);
        await IndexLibrary.ShowToast('error', 'Could not reach the service to select this prototype.');
        return;
    }

    // Re-render so every row's button reflects the new selection.
    await updateDeveloperAgentTableBody(latestDeveloperExecutions, true);
}

function updateDeveloperExecutionSummary(agentExecutions) {
    const executions = Array.isArray(agentExecutions) ? agentExecutions : [];
    const total = executions.length;
    const completed = executions.filter(e => (e.status || '').toLowerCase() === 'completed').length;
    const failed = executions.filter(e => (e.status || '').toLowerCase() === 'failed').length;
    const running = executions.filter(e => {
        const status = (e.status || '').toLowerCase();
        return status !== 'completed' && status !== 'failed';
    }).length;
    const finished = completed + failed;
    const successRate = finished > 0 ? Math.round((completed / finished) * 100) : 0;

    $('#daSummaryTotal').text(total);
    $('#daSummaryRunning').text(running);
    $('#daSummaryCompleted').text(completed);
    $('#daSummaryFailed').text(failed);
    $('#daSummarySuccessRateBar').css('width', `${successRate}%`);
    $('#daSummarySuccessRateText').text(`${successRate}%`);
}

function applyDeveloperExecutionFilter(executions) {
    const source = Array.isArray(executions) ? executions : [];
    if (currentStatusFilter === 'all') return source;
    if (currentStatusFilter === 'running') {
        return source.filter(e => {
            const status = (e.status || '').toLowerCase();
            return status !== 'completed' && status !== 'failed';
        });
    }

    return source.filter(e => (e.status || '').toLowerCase() === currentStatusFilter);
}

function applyDeveloperSearchFilter(executions) {
    const source = Array.isArray(executions) ? executions : [];
    const search = currentSearchTerm.trim().toLowerCase();
    if (!search) return source;

    return source.filter((execution) => {
        const haystack = [
            execution.createdBy,
            execution.projectName,
            execution.status,
            execution.output,
            execution.codeReviewFeedback,
            renderExecutionStatus(execution)
        ].map(v => (v ?? '').toString().toLowerCase()).join(' ');

        return haystack.includes(search);
    });
}

function getDeveloperSortValue(execution, sortField) {
    switch (sortField) {
        case 'QueuedBy':
            return (execution.createdBy || '').toLowerCase();
        case 'Project':
            return (execution.projectName || '').toLowerCase();
        case 'Queued':
            return parseUTCDate(execution.createdDate)?.getTime() || 0;
        case 'Started':
            return parseUTCDate(execution.startedAt)?.getTime() || 0;
        case 'Completed':
            return parseUTCDate(execution.completedAt)?.getTime() || 0;
        case 'Duration':
            return Number(execution.durationInSeconds) || 0;
        case 'Status':
            return (renderExecutionStatus(execution) || '').toLowerCase();
        case 'Output':
            return (execution.output || execution.codeReviewFeedback || '').toLowerCase();
        default:
            return '';
    }
}

function applyDeveloperExecutionSort(executions) {
    const source = Array.isArray(executions) ? [...executions] : [];

    source.sort((a, b) => {
        const aVal = getDeveloperSortValue(a, currentSortField);
        const bVal = getDeveloperSortValue(b, currentSortField);

        if (aVal < bVal) return currentSortDirection === 'asc' ? -1 : 1;
        if (aVal > bVal) return currentSortDirection === 'asc' ? 1 : -1;
        return 0;
    });

    return source;
}

function updateDeveloperSortHeaderUi() {
    let activeColumnIndex = -1;

    $('#DeveloperAgentExecutionTable th[data-sort]').each(function () {
        const th = $(this);
        const field = (th.data('sort') || '').toString();
        const icon = th.find('.sort-icon i');

        if (!icon.length) return;

        if (field === currentSortField) {
            activeColumnIndex = th.index();
            icon.removeClass('bi-caret-up-fill bi-caret-down-fill').addClass(
                currentSortDirection === 'asc' ? 'bi-caret-up-fill' : 'bi-caret-down-fill'
            );
            th.addClass('active-sort active-sort-column column-center');
        } else {
            icon.removeClass('bi-caret-up-fill bi-caret-down-fill').addClass('bi-caret-down-fill');
            th.removeClass('active-sort active-sort-column column-center');
        }
    });

    const tableRows = $('#DeveloperAgentExecutionTableBody tr');
    tableRows.children('td').removeClass('active-sort-column column-center');

    if (activeColumnIndex >= 0) {
        tableRows.each(function () {
            $(this).children().eq(activeColumnIndex).addClass('active-sort-column column-center');
        });
    }
}

function normalizeStepKey(stepName) {
    if (!stepName) return '';
    return stepName.toString().split(':')[0].trim();
}

function getStepProgressState(element) {
    const executionStatus = (element?.status || '').toLowerCase();
    const normalizedCurrentStep = normalizeStepKey(element?.currentStep);
    const currentStepIndex = STEP_SEQUENCE.findIndex(step => step.key === normalizedCurrentStep);

    const isCompleted = executionStatus === 'completed';
    const isFailed = executionStatus === 'failed';
    const isRunning = executionStatus === 'running';

    let activeIndex = -1;
    if (isCompleted) {
        activeIndex = STEP_SEQUENCE.length - 1;
    } else if (isRunning || isFailed) {
        activeIndex = currentStepIndex >= 0 ? currentStepIndex : 0;
    }

    return { activeIndex, isCompleted, isFailed };
}

function buildStepProgressMarkup(element) {
    const { activeIndex, isCompleted, isFailed } = getStepProgressState(element);

    const pointsHtml = STEP_SEQUENCE.map((step, index) => {
        const classes = ['da-step-point'];

        if (isCompleted || index < activeIndex) {
            classes.push('done');
        }
        if (!isCompleted && index === activeIndex) {
            classes.push('current');
        }
        if (isFailed && index === activeIndex) {
            classes.push('failed');
        }

        const escapedStepLabel = IndexLibrary.escapeHtml(step.label);
        const stepTooltipHtml = `<div class='queued-by-tooltip-content'><span class='queued-by-tooltip-header'><i class='bi bi-diagram-3'></i>Step</span><span class='queued-by-tooltip-name'><i class='bi bi-signpost-split'></i>${escapedStepLabel}</span></div>`;
        return `<span class="${classes.join(' ')}" data-bs-toggle="tooltip" data-bs-title="${stepTooltipHtml}" data-bs-html="true" data-bs-custom-class="queued-by-tooltip" data-bs-placement="top"></span>`;
    }).join('');

    const progressStateClass = isCompleted ? 'completed' : (isFailed ? 'failed' : 'running');
    return `<div class="da-step-progress ${progressStateClass}">${pointsHtml}</div>`;
}

function getDeveloperStatusInfo(element) {
    const value = (element?.status || 'Unknown').toLowerCase();
    const statusLabel = renderExecutionStatus(element);

    if (value === 'completed') return { css: 'completed', icon: 'bi-check-circle-fill', label: statusLabel };
    if (value === 'failed') return { css: 'failed', icon: 'bi-x-octagon-fill', label: statusLabel };
    if (value === 'pending') return { css: 'queued', icon: 'bi-hourglass-split', label: statusLabel };

    return { css: 'running', icon: 'bi-arrow-repeat', label: statusLabel };
}

function updateDeveloperViewControlsUi() {
    $('.cr-filter-chip').removeClass('active');
    $(`.cr-filter-chip[data-filter='${currentStatusFilter}']`).addClass('active');

    $('.cr-view-btn').removeClass('active');
    $(`.cr-view-btn[data-view='${currentViewMode}']`).addClass('active');

    if (currentViewMode === 'cards') {
        $('#DeveloperAgentTableScroll').hide();
        $('#DeveloperAgentCardView').show();
    } else {
        $('#DeveloperAgentCardView').hide();
        $('#DeveloperAgentTableScroll').show();
    }
}

function bindDeveloperDisplayControls() {
    $('#daFilterChips').off('click', '.cr-filter-chip').on('click', '.cr-filter-chip', async function () {
        currentStatusFilter = ($(this).data('filter') || 'all').toString();
        await updateDeveloperAgentTableBody(latestDeveloperExecutions);
    });

    $('#daViewSwitch').off('click', '.cr-view-btn').on('click', '.cr-view-btn', async function () {
        currentViewMode = ($(this).data('view') || 'table').toString();
        await updateDeveloperAgentTableBody(latestDeveloperExecutions);
    });

    $('#DeveloperAgentExecutionTable')
        .off('click', 'th[data-sort]')
        .on('click', 'th[data-sort]', async function () {
            const selectedField = ($(this).data('sort') || '').toString();
            if (!selectedField) return;

            if (selectedField === currentSortField) {
                currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
            } else {
                currentSortField = selectedField;
                currentSortDirection = 'asc';
            }

            await updateDeveloperAgentTableBody(latestDeveloperExecutions);
        });

    $(document).off('input', '#daSearchInput').on('input', '#daSearchInput', async function () {
        currentSearchTerm = ($(this).val() || '').toString();
        await updateDeveloperAgentTableBody(latestDeveloperExecutions);
    });

    // aria-disabled rather than the disabled attribute: a disabled button swallows the events
    // Bootstrap needs, and the "already selected" tooltip is the whole point of hovering it.
    $(document).off('click', '.proto-select-btn').on('click', '.proto-select-btn', async function () {
        const button = $(this);
        if (button.attr('aria-disabled') === 'true') return;
        await selectPrototype(button);
    });

    // Not async: the tab must be opened synchronously or it is blocked as an unsolicited pop-up.
    $(document).off('click', '.proto-view-btn').on('click', '.proto-view-btn', function () {
        openPrototype($(this), window.open('', '_blank'));
    });

    $(document).off('click', '#daResetGridBtn').on('click', '#daResetGridBtn', async function () {
        currentStatusFilter = 'all';
        currentViewMode = 'table';
        currentSearchTerm = '';
        $('#daSearchInput').val('');
        await updateDeveloperAgentTableBody(latestDeveloperExecutions);
    });
}

// What to show in the Status column. While Running, show the current step's friendly label
// (falling back to "Running" if the step is unknown / not yet recorded). Pending -> "Queued".
function renderExecutionStatus(element) {
    if (element.status === "Running") {
        return STEP_LABELS[element.currentStep] ?? "Running";
    }
    if (element.status === "Pending") {
        return "Queued";
    }
    return element.status;
}
IndexLibrary.InitializeSDK().then(async function () {
    await LoadConfigFile();
});

$("#DeveloperAgentListBody").hide();

function setButtonState(buttonSelector, isEnabled) {
    const btn = $(buttonSelector);
    if (btn.length) {
        btn.prop('disabled', !isEnabled);
        btn.css('opacity', isEnabled ? '1' : '0.5');
        btn.css('cursor', isEnabled ? 'pointer' : 'not-allowed');
        btn.css('pointer-events', isEnabled ? 'auto' : 'none');
    }
}

async function LoadConfigFile() {
    var orgName = await IndexLibrary.GetAccountNameAsync();
    try {
        $('#trialLicensePill').hide();

        const config = await IndexLibrary.GetExtensionConfig();
        baseAPIUrl = config.apiBaseUrl;
        orchestratorApiBaseUrl = config.orchestratorApiBaseUrl;
        orchestratorApiKey = config.orchestratorApiKey;
        publisherId = config.publisherId;
        extensionId = config.extensionId;

        const getLicenseResponse = await IndexLibrary.PostRequest({
            endpoint: '/api/License/GetLicense',
            body: { organizationName: orgName }
        });

        if (getLicenseResponse.status == 404) {
            $("#startTrialMessage").show();
            CreateTrialLicense();
            $("#main").hide();
            return;
        }

        const licenseData = await getLicenseResponse.json();

        if (licenseData.status !== undefined && licenseData.status == 404) {
            $("#startTrialMessage").show();
            CreateTrialLicense();
            $("#main").hide();
            return;
        }

        // This tab hosts both agents, so either license grants access to it.
        const [daLicensedResponse, paLicensedResponse] = await Promise.all([
            IndexLibrary.GetRequest({
                endpoint: `/api/License/IsUserLicensed?organizationName=${orgName}&email=${IndexLibrary.GetUserEmail()}&moduleUserType=daUserType`
            }),
            IndexLibrary.GetRequest({
                endpoint: `/api/License/IsUserLicensed?organizationName=${orgName}&email=${IndexLibrary.GetUserEmail()}&moduleUserType=paUserType`
            })
        ]);
        const isLicensed = (await daLicensedResponse.text()).trim().toLowerCase() === 'true'
            || (await paLicensedResponse.text()).trim().toLowerCase() === 'true';

        if (!isLicensed) {
            $("#startTrialMessage").hide();
            $("#licenseExpired").hide();
            $("#unauthorized").hide();
            $("#authorizationErrorMessage").show();
            $("#authorizationErrorMessage").text("You are not authorized to perform this action. Please contact your organization administrator.");
            await PopulateDeveloperAgentExecutionTable();
            $("#loadingSpinner").hide();
            setButtonState("#btnQueueDeveloperAgent", false);
            $("#main").show();
            return;
        }

        const orgLicenseValidityResponse = await IndexLibrary.GetRequest({
            endpoint: `/api/License/IsOrgLicenseValid?organizationName=${orgName}`
        });
        const orgLicenseValidityData = await orgLicenseValidityResponse.text();

        if (orgLicenseValidityData.toLowerCase() == 'false') {
            $("#startTrialMessage").show();
            $("#startTrialMessage").text("Your license is invalid or expired. Please contact your organization administrator.");
            $("#licenseExpired").hide();
            $("#unauthorized").hide();
            $("#authorizationErrorMessage").hide();
            await PopulateDeveloperAgentExecutionTable();
            await updateDeveloperAgentTableBody();
            startGlobalTimer();
            $("#main").show();
            setButtonState("#btnQueueDeveloperAgent", false);
            return;
        }

        let isOrgLicenseExpired = false;
        if (licenseData.expiryDateTimeUtc) {
            const expiryDate = new Date(licenseData.expiryDateTimeUtc);
            const today = new Date();
            isOrgLicenseExpired = today > expiryDate;
        }

        if (licenseData.licenseType == 0) {
            const licenseExpiryDate = new Date(licenseData.expiryDateTimeUtc);
            if (isOrgLicenseExpired) {
                $('#trialLicensePill').hide();
                $("#startTrialMessage").show();
                $("#startTrialMessage").text("Your license is invalid or expired. Please contact your organization administrator.");
            } else {
                const trialMessage = `Trial until ${licenseExpiryDate.toLocaleDateString('en-US')}`;
                $('#trialLicensePillText').text(trialMessage);
                $('#trialLicensePill').show();
                $("#startTrialMessage").hide();
            }
        } else {
            $('#trialLicensePill').hide();
            $("#startTrialMessage").hide();
        }

        await PopulateDeveloperAgentExecutionTable();
        await updateDeveloperAgentTableBody();
        startGlobalTimer();

        $("#main").show();
        $("#unauthorized").hide();
        $("#licenseExpired").hide();
        $("#authorizationErrorMessage").hide();

        const [daTypeResponse, paTypeResponse] = await Promise.all([
            IndexLibrary.GetRequest({
                endpoint: `/api/License/GetUserLicenseType?organizationName=${orgName}&email=${IndexLibrary.GetUserEmail()}&fieldName=daUserType`
            }),
            IndexLibrary.GetRequest({
                endpoint: `/api/License/GetUserLicenseType?organizationName=${orgName}&email=${IndexLibrary.GetUserEmail()}&fieldName=paUserType`
            }),
            loadOrganizationScope(),
            loadCurrentProjectContext()
        ]);
        const hasLicenseType = (text) => {
            const trimmed = text.trim().toLowerCase();
            return trimmed !== 'null' && trimmed !== '' && trimmed !== 'undefined';
        };
        hasDeveloperAgentLicense = hasLicenseType(await daTypeResponse.text()) && !isOrgLicenseExpired;
        hasPrototypeAgentLicense = hasLicenseType(await paTypeResponse.text()) && !isOrgLicenseExpired;
        setButtonState("#btnQueueDeveloperAgent", hasDeveloperAgentLicense || hasPrototypeAgentLicense);

        // The table above was drawn before these flags resolved, so anything gated on a license
        // (the per-row prototype Select button) was skipped. Redraw from the executions already
        // fetched — no second round trip, and the selection state read is still current.
        await updateDeveloperAgentTableBody(latestDeveloperExecutions, true);
    } catch (error) {
        await IndexLibrary.ShowToast('error', 'Failed to initialize. Please refresh the page.');
    }
}

// ProjectScope: 0 = Organization, 1 = Project.
async function loadOrganizationScope() {
    try {
        const response = await IndexLibrary.GetRequest({ endpoint: '/api/Settings/Get' });
        if (!response.ok) {
            console.warn(`Could not read the organization scope setting (HTTP ${response.status}); keeping project scope.`);
            return;
        }

        const scope = await response.json().then(s => s?.projectScope);

        // A number today; a string if the API ever gains a JsonStringEnumConverter.
        isProjectScopedOrg = typeof scope === 'string'
            ? scope.trim().toLowerCase() !== 'organization'
            : Number(scope) !== 0;
    } catch (error) {
        console.warn('Could not read the organization scope setting; keeping project scope.', error);
    }
}

async function loadCurrentProjectContext() {
    try {
        currentProjectContext = await IndexLibrary.GetCurrentProject();
    } catch (error) {
        currentProjectContext = { id: '', name: '' };
        console.warn('Could not resolve the current project.', error);
    }
}

async function PopulateDeveloperAgentExecutionTable() {
    const allUsers = await IndexLibrary.getAllUsers();
    userLookup = IndexLibrary.buildUserLookup(allUsers);
    $("#loadingSpinner").show();

    $("#DeveloperAgentListBody").html(DEVELOPER_AGENT_TABLE_HTML);
    bindDeveloperDisplayControls();
    updateDeveloperViewControlsUi();
    $("#DeveloperAgentListBody").show();

}
// useKnownSelection: skip re-reading the work item when the caller just performed the change and
// already holds the authoritative answer — avoids a flicker back to the old state if Azure DevOps
// has not surfaced the new attachment yet, which would invite a pointless second click.
async function updateDeveloperAgentTableBody(agentExecutions = null, useKnownSelection = false) {
    if (!agentExecutions) {
        agentExecutions = await getAgentExecutions();
    }
    latestDeveloperExecutions = Array.isArray(agentExecutions) ? agentExecutions : [];
    lastExecutionsSignature = buildExecutionsSignature(latestDeveloperExecutions);
    console.log("body executaion", agentExecutions)
    updateDeveloperExecutionSummary(agentExecutions);

    // Read which prototype is currently attached before drawing the buttons, so their state is
    // right even when the change was made by someone else or in another tab.
    if (!useKnownSelection) {
        await refreshSelectedPrototype();
    }

    const filteredByStatus = applyDeveloperExecutionFilter(agentExecutions);
    const filteredExecutions = applyDeveloperSearchFilter(filteredByStatus);
    const sortedExecutions = applyDeveloperExecutionSort(filteredExecutions);

    const tbody = $("#DeveloperAgentExecutionTableBody");
    const cardView = $("#DeveloperAgentCardView");
    tbody.empty();
    cardView.empty();

    await IndexLibrary.loadAvatarsFor((sortedExecutions || []).map(execution => execution.createdBy));

    sortedExecutions?.forEach((element) => {
        const rawQueued = element.createdDate;
        const rawStarted = element.startedAt;
        const rawCompleted = element.completedAt;

        const queuedStr = formatDateTimeLocal(rawQueued);   // pass raw string
        const startedStr = formatDateTimeLocal(rawStarted); // pass raw string
        const completedStr = formatDateTimeLocal(rawCompleted);
        const userEmail = element.createdBy?.toLowerCase();
        const user = userLookup[userEmail];
        const userName = user ? user.name || element.createdBy : element.createdBy;
        const avatarLink = IndexLibrary.getCachedAvatar(element.createdBy);
        const userNameTrimmed = userName?.trim();
        const userEmailTrimmed = element.createdBy?.trim();

        const statusInfo = getDeveloperStatusInfo(element);
        const showEmail =
            userEmailTrimmed &&
            userNameTrimmed &&
            userNameTrimmed.toLowerCase() !== userEmailTrimmed.toLowerCase();
        const queuedByTooltipHtml = IndexLibrary.buildQueuedByTooltipHtml({
            displayName: userNameTrimmed || userEmailTrimmed || "Unknown user",
            email: userEmailTrimmed || "",
            showEmail,
            statusCss: statusInfo.css,
            statusIcon: statusInfo.icon
        });

        let userHtml = "";
        if (avatarLink) {
            userHtml = `
                    <div class="user-cell" data-bs-toggle="tooltip" data-bs-title="${queuedByTooltipHtml}" data-bs-html="true" data-bs-custom-class="queued-by-tooltip" data-bs-placement="top">
                        <img src="${avatarLink}" class="avatar-img" />
                        <span>${userName}</span>
                    </div>
                `;
        } else {
            userHtml = `
                    <div class="user-cell" data-bs-toggle="tooltip" data-bs-title="${queuedByTooltipHtml}" data-bs-html="true" data-bs-custom-class="queued-by-tooltip" data-bs-placement="top">
                        <span class="avatar">${getInitials(element.createdBy)}</span>
                        <span>${userName}</span>
                    </div>
                `;
        }

        // Parsed from the same value that is displayed: a prototype run reports its result in
        // codeReviewFeedback, not output, so reading output alone finds nothing.
        const rawOutput = element.output ?? element.codeReviewFeedback ?? "-";
        const deployment = parsePrototypeDeployment(rawOutput);
        const displayedOutput = stripDeployedSiteLine(rawOutput, deployment);
        const outputText = linkifyUrls(IndexLibrary.escapeHtml(displayedOutput)).replace(/\r?\n/g, '<br />');
        const outputTooltip = IndexLibrary.buildReviewDetailsTooltipHtml(displayedOutput);
        // Sibling of the feedback cell, not a child: the cell carries its own full-output
        // tooltip, and nesting would make the button inherit it.
        const prototypeButtons = buildPrototypeViewButton(deployment) + buildPrototypeSelectButton(element, deployment);

        const rowMarkup = `
            <tr>
                <td>${userHtml}</td>
                <td>${element.projectName ?? ""}</td>
                <td>${queuedStr}</td>
                <td>${startedStr}</td>
                 <td>${completedStr}</td>
                <td>
                ${(() => {
                let durationHtml = "-";
                if (element.startedAt && element.status !== "Completed" && element.status !== "Failed") {
                    const startTime = new Date(
                        element.startedAt.endsWith('Z') ? element.startedAt : element.startedAt + 'Z'
                    ).getTime();

                    const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
                    durationHtml = `
                    <span class="running-duration">
                        <i class="bi bi-stopwatch"></i>  <span id="timer-${element.id}" data-started="${element.startedAt}">${formatDuration(elapsedSeconds)}</span>
                    </span>
                `;
                } else {
                    if (runningTimers[element.id]) delete runningTimers[element.id];
                    durationHtml = `<i class="bi bi-clock"></i> ${formatDuration(element.durationInSeconds)}`;
                }
                return durationHtml;
            })()}
                </td>
                <td>
                    <div class="da-status-cell">
                        <span class="status-pill ${statusInfo.css}">
                            <i class="bi ${statusInfo.icon}"></i>
                            ${IndexLibrary.escapeHtml(statusInfo.label)}
                        </span>
                        ${buildStepProgressMarkup(element)}
                    </div>
                </td>
                <td>
                    <div class="review-feedback-cell" data-bs-toggle="tooltip" data-bs-title="${outputTooltip}" data-bs-html="true" data-bs-custom-class="review-details-tooltip" data-bs-placement="top">${outputText}</div>
                    ${prototypeButtons}
                </td>
            </tr>
        `;

        const cardMarkup = `
            <article class="cr-execution-card cr-card-status-${statusInfo.css}">
                <div class="cr-card-top">
                    <div>${userHtml}</div>
                    <span class="status-pill ${statusInfo.css}"><i class="bi ${statusInfo.icon}"></i>${IndexLibrary.escapeHtml(statusInfo.label)}</span>
                </div>
                ${buildStepProgressMarkup(element)}
                <div class="cr-card-meta">
                    <div><span>Project</span><div class="cr-card-meta-value">${IndexLibrary.escapeHtml(element.projectName || '-')}</div></div>
                    <div><span>Queued</span><div class="cr-card-meta-value">${queuedStr}</div></div>
                    <div><span>Started</span><div class="cr-card-meta-value">${startedStr}</div></div>
                    <div><span>Completed</span><div class="cr-card-meta-value">${completedStr}</div></div>
                </div>
                <div class="cr-card-feedback" data-bs-toggle="tooltip" data-bs-title="${outputTooltip}" data-bs-html="true" data-bs-custom-class="review-details-tooltip" data-bs-placement="top">${outputText}</div>
                ${prototypeButtons}
            </article>
        `;

        tbody.append(rowMarkup);
        cardView.append(cardMarkup);
    });

    updateDeveloperViewControlsUi();
    updateDeveloperSortHeaderUi();

    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
        const tooltip = bootstrap.Tooltip.getInstance(el);
        if (tooltip) tooltip.dispose();
    });
    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
        new bootstrap.Tooltip(el);
    });

    manageTablePolling(agentExecutions);
    $("#loadingSpinner").hide();
    $("#DeveloperAgentListBody").show();

}

async function refreshDeveloperAgentTable() {
    const refreshBtn = $("#btnRefreshDeveloperAgent");
    try {
        refreshBtn.prop("disabled", true);
        $("#loadingSpinner").show();
        const executions = await getAgentExecutions();
        await updateDeveloperAgentTableBody(executions);
        await IndexLibrary.ShowToast('success', 'Developer agent data refreshed.');
    } catch (error) {
        await IndexLibrary.ShowToast('error', 'Unable to refresh developer agent table.');
    } finally {
        refreshBtn.prop("disabled", false);
        $("#loadingSpinner").hide();
    }
}
function manageTablePolling(agentExecutions) {

    const hasRunning = agentExecutions.some(e =>
        e.startedAt && e.status !== "Completed" && e.status !== "Failed"
    );
    if (hasRunning) {
        // Start interval if not already started
        if (!refreshInterval) {
            refreshInterval = setInterval(async () => {
                // Cheap check first — a few hundred bytes instead of the full list of every
                // execution ever queued against this work item. Only fetch (and re-render) the
                // full list once something the table shows has actually changed. Any failure
                // falls through to the full fetch, so an older API build still works.
                try {
                    const statuses = await getAgentExecutionStatuses();
                    if (buildExecutionsSignature(statuses) === lastExecutionsSignature) return;
                } catch {
                    // fall through to the full fetch
                }

                const executions = await getAgentExecutions();

                await updateDeveloperAgentTableBody(executions);
            }, 5000);
        }
    } else {
        // Stop interval if no running executions
        if (refreshInterval) {
            clearInterval(refreshInterval);
            refreshInterval = null;
        }
    }
}
function startGlobalTimer() {
    if (uiTimerStarted) return;
    uiTimerStarted = true;

    setInterval(() => {
        document.querySelectorAll("[id^='timer-']").forEach(el => {
            const startedAt = el.getAttribute("data-started");
            if (!startedAt) return;

            const startTime = new Date(
                startedAt.endsWith('Z') ? startedAt : startedAt + 'Z'
            ).getTime();

            const seconds = Math.floor((Date.now() - startTime) / 1000);
            el.innerText = formatDuration(Math.max(0, seconds));
        });
    }, 1000);
}
function formatDuration(seconds) {
    const hrs = Math.floor(seconds / 3600);
    const mins = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;

    let parts = [];

    if (hrs > 0) parts.push(`${hrs}h`);
    if (mins > 0 || hrs > 0) parts.push(`${mins}m`);
    parts.push(`${secs}s`);

    return parts.join(" ");
}

function getInitials(email) {
    if (!email) return "?";
    const parts = email.split("@")[0].split(".");
    if (parts.length >= 2) {
        return (parts[0][0] + parts[1][0]).toUpperCase();
    }
    return email.substring(0, 2).toUpperCase();
}

async function getAgentExecutions() {
    const workItemId = await IndexLibrary.CurrentWorkItem();
    const reviewRequest = {
        organizationId: IndexLibrary.GetAccountId(),
        workItemOrPullRequestId: workItemId,
    };

    const query = new URLSearchParams(reviewRequest).toString();

    const response = await IndexLibrary.GetRequest({
        baseAPIUrl: orchestratorApiBaseUrl,
        endpoint: `/api/agentexecution/get?${query}`,
        xApiKey: orchestratorApiKey
    });

    if (!response.ok) {
        if (response.status === 400) {
            var responseJson = await response.json();
            await IndexLibrary.ShowToast('error', responseJson.message);
        } else {
            throw new Error(`API call failed: ${response.status}`);
        }
    } else {
        return await response.json();
    }
}

// Lightweight sibling of /get used by the 5s poll: id + status + current step only.
async function getAgentExecutionStatuses() {
    const workItemId = await IndexLibrary.CurrentWorkItem();
    const query = new URLSearchParams({
        organizationId: IndexLibrary.GetAccountId(),
        workItemOrPullRequestId: workItemId,
    }).toString();

    const response = await IndexLibrary.GetRequest({
        baseAPIUrl: orchestratorApiBaseUrl,
        endpoint: `/api/agentexecution/status?${query}`,
        xApiKey: orchestratorApiKey
    });

    if (!response.ok) {
        throw new Error(`API call failed: ${response.status}`);
    }

    return await response.json();
}

// Everything the table's contents depend on. Works against both the full list and the status
// list because /status returns the same rows, in the same order, with these three fields.
function buildExecutionsSignature(executions) {
    return (executions ?? [])
        .map(e => `${e.id}:${e.status}:${e.currentStep ?? ""}`)
        .join("|");
}

async function CreateTrialLicense() {
    await IndexLibrary.AutoCreateTrialLicense(true);
}

function sortByName(items) {
    return [...(items ?? [])].sort((a, b) =>
        (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base', numeric: true })
    );
}

// The CSS width must stay free of !important or the inline size the drag writes is ignored.
function makeQueuePopupResizable() {
    const element = Swal.getPopup();
    if (!element || typeof $(element).resizable !== 'function') return;

    requestAnimationFrame(() => {
        const popup = $(element);
        if (popup.hasClass('ui-resizable')) return;

        // SweetAlert centres the popup in a grid, so a growing box drifts back under the cursor
        // at half speed. Pinning it where it opened makes the drag track 1:1.
        const rect = element.getBoundingClientRect();
        const capHeight = (top) => popup.css('maxHeight', `${Math.max(300, window.innerHeight - top - 16)}px`);

        popup.css({
            position: 'fixed',
            margin: 0,
            top: `${rect.top}px`,
            left: `${rect.left}px`,
            width: `${rect.width}px`
        });
        capHeight(rect.top);

        popup.resizable({
            handles: 'n, e, s, w, ne, se, sw, nw',
            minWidth: 420,
            minHeight: 300,
            maxWidth: Math.max(420, window.innerWidth - 16),
            maxHeight: Math.max(300, window.innerHeight - 16),
            resize: (event, ui) => capHeight(ui.position.top)
        });
    });
}

// Kept in sync with the agent's default DeveloperAgent:BugLikeWorkItemTypes ("Bug,Issue").
const BUG_LIKE_WORK_ITEM_TYPES = ['bug', 'issue'];

// Mirrors DevAzurePrUrl / VsComPrUrl in the agent's AzureDevOpsService. Captures the repo name.
const PR_URL_PATTERNS = [
    /dev\.azure\.com\/[^/\s"'<>]+\/[^/\s"'<>]+\/_git\/([^/\s"'<>]+)\/pullrequest\/\d+/i,
    /[^./\s"'<>]+\.visualstudio\.com\/[^/\s"'<>]+\/_git\/([^/\s"'<>]+)\/pullrequest\/\d+/i
];

// vstfs:///Git/PullRequestId/{projectGuid}%2F{repoGuid}%2F{prId} — separators occasionally arrive raw.
const PR_ARTIFACT_PATTERN = /PullRequestId\/([^/%]+)(?:%2F|\/)([^/%]+)(?:%2F|\/)(\d+)/i;

function safeDecodeUriComponent(value) {
    try {
        return decodeURIComponent(value);
    } catch {
        return value;
    }
}

// The relation gives a repo GUID; the large-repository list holds names.
async function resolveRepositoryNameById(projectId, repositoryId) {
    try {
        const repos = await IndexLibrary.getReposByProject(safeDecodeUriComponent(projectId));
        const wanted = safeDecodeUriComponent(repositoryId).toLowerCase();
        return repos.find(repo => (repo.id || '').toLowerCase() === wanted)?.name || null;
    } catch (error) {
        console.warn('Could not resolve the linked pull request\'s repository name — the run will use the standard Developer agent.', error);
        return null;
    }
}

// Same two sources, same order, as the agent's triage. Fails open to the standard flow.
async function resolveLinkedPullRequest() {
    try {
        const workItemId = await IndexLibrary.CurrentWorkItem();
        const workItem = await IndexLibrary.GetWorkItemWithRelations(Number(workItemId));

        // Description first — it carries the repo name outright, so no extra call is needed.
        const description = workItem?.fields?.['System.Description'] ?? '';
        for (const pattern of PR_URL_PATTERNS) {
            const match = pattern.exec(description);
            if (match) return { found: true, repositoryName: safeDecodeUriComponent(match[1]) };
        }

        let sawPullRequestRelation = false;
        for (const relation of (workItem?.relations || [])) {
            if ((relation.rel || '').toLowerCase() !== 'artifactlink') continue;
            if ((relation.attributes?.name || '').toLowerCase() !== 'pull request') continue;

            sawPullRequestRelation = true;
            const match = PR_ARTIFACT_PATTERN.exec(relation.url || '');
            if (!match) continue;

            const repositoryName = await resolveRepositoryNameById(match[1], match[2]);
            if (repositoryName) return { found: true, repositoryName };
        }

        return sawPullRequestRelation
            ? { found: true, repositoryName: null }
            : { found: false, repositoryName: null };
    } catch (error) {
        console.warn('Could not check the work item for a linked pull request — using the standard queue flow.', error);
        return { found: false, repositoryName: null };
    }
}

async function queueDeveloperAgentWorkItem() {
    const workItemId = await IndexLibrary.CurrentWorkItem();
    const organizationId = IndexLibrary.GetAccountId();
    const organizationName = await IndexLibrary.GetAccountNameAsync();

    // Bug/Issue fast path, ONLY for a defect raised from a pull request: the PR is what supplies the
    // project, repository and branch. Without one it falls through to the standard flow below.
    //  • Developer Agent (v2) resolves project, repository and branch from the linked pull request
    //    and commits the fix — no further input needed.
    //  • Prototype Agent (v1) doesn't read the PR; it generates a prototype from the work item
    //    and uses the work item's own project.
    const workItemType = ((await IndexLibrary.GetCurrentWorkItemType()) || '').trim();
    const isBugLike = BUG_LIKE_WORK_ITEM_TYPES.includes(workItemType.toLowerCase());
    const linkedPullRequest = isBugLike
        ? await resolveLinkedPullRequest()
        : { found: false, repositoryName: null };

    if (linkedPullRequest.found) {
        // Offer only the versions the user is licensed for. Developer first, so it's the default
        // (zero-input) selection for a bug when both licenses are present.
        const bugVersionOptions = [
            ...(hasDeveloperAgentLicense ? ['<option value="v2">Developer Agent</option>'] : []),
            ...(hasPrototypeAgentLicense ? ['<option value="v1">Prototype Agent</option>'] : [])
        ].join('');

        if (!bugVersionOptions) {
            await IndexLibrary.ShowToast('error', 'You do not have a license for any agent version. Please contact your organization administrator.');
            return;
        }

        const { value: bugFormValues, isConfirmed } = await Swal.fire({
            title: '<i class="bi bi-bug da-queue-title-icon"></i> Queue Developer Agent',
            html: `
                <div class="da-swal-body">
                    <div class="da-swal-field">
                        <label class="swal-label"><i class="bi bi-cpu-fill"></i> Agent Version</label>
                        <select id="swal-bug-version" class="swal-select">
                            ${bugVersionOptions}
                        </select>
                    </div>
                    <div class="da-swal-auto-project" id="swal-bug-project-note" style="display:none;">
                        <i class="bi bi-folder2-open"></i>
                        <span>Project</span>
                        <strong>${IndexLibrary.escapeHtml(currentProjectContext.name || 'Unavailable')}</strong>
                    </div>
                </div>
            `,
            customClass: {
                popup: 'da-queue-popup da-queue-popup-compact',
                title: 'da-queue-title',
                htmlContainer: 'da-queue-html',
                confirmButton: 'da-queue-btn-confirm',
                cancelButton: 'da-queue-btn-cancel',
                actions: 'da-queue-actions',
            },
            showCancelButton: true,
            confirmButtonText: '<i class="bi bi-send-fill"></i> Queue',
            cancelButtonText: 'Cancel',
            buttonsStyling: false,
            focusConfirm: false,
            didOpen: () => {
                const versionSelect = document.getElementById('swal-bug-version');
                const projectNote = document.getElementById('swal-bug-project-note');

                const sync = () => {
                    projectNote.style.display = versionSelect.value === 'v1' ? '' : 'none';
                };
                versionSelect.addEventListener('change', sync);
                sync();

                makeQueuePopupResizable();
            },
            preConfirm: () => {
                const version = document.getElementById('swal-bug-version').value;
                if (version === 'v1') {
                    if (!currentProjectContext.id) {
                        Swal.showValidationMessage('The current project could not be resolved. Please reload the work item and try again.');
                        return false;
                    }
                    return { version, projectId: currentProjectContext.id, projectName: currentProjectContext.name };
                }
                return { version, projectId: '', projectName: '' };
            },
        });

        if (!isConfirmed || !bugFormValues) return;

        await IndexLibrary.QueueDeveloperAgentWorkItem(
            workItemId,
            bugFormValues.projectName,   // '' for Developer (resolved from the linked PR)
            bugFormValues.projectId,     // '' for Developer (resolved from the linked PR)
            organizationId,
            organizationName,
            [],   // repositoryNames — resolved from the linked PR (Developer) / n/a (Prototype)
            {},   // repositoryRoles — n/a for bugs
            bugFormValues.version,
            linkedPullRequest.repositoryName   // routing hint only — see QueueDeveloperAgentWorkItem
        );

        await new Promise(resolve => setTimeout(resolve, 1000));
        await updateDeveloperAgentTableBody();
        return;
    }

    if (!currentProjectContext.id) {
        await IndexLibrary.ShowToast('error', 'The current project could not be resolved. Please reload the work item and try again.');
        return;
    }

    // Project scope pins the dialog to this work item's project; org scope keeps the picker.
    const selectableProjects = isProjectScopedOrg
        ? [currentProjectContext]
        : sortByName(await IndexLibrary.getAllProjectsWithNames());

    const projectOptions = (isProjectScopedOrg ? '' : '<option value="">Select a project</option>')
        + selectableProjects.map(p =>
            `<option value="${IndexLibrary.escapeHtml(p.id)}" data-name="${IndexLibrary.escapeHtml(p.name)}">${IndexLibrary.escapeHtml(p.name)}</option>`
        ).join('');

    const ROLE_OPTIONS = ['General', 'Frontend', 'Backend', 'Shared'];

    // Only offer the agent versions the user is licensed for.
    const versionOptions = [
        ...(hasPrototypeAgentLicense ? ['<option value="v1">Prototype Agent</option>'] : []),
        ...(hasDeveloperAgentLicense ? ['<option value="v2">Developer Agent</option>'] : [])
    ].join('');

    if (!versionOptions) {
        await IndexLibrary.ShowToast('error', 'You do not have a license for any agent version. Please contact your organization administrator.');
        return;
    }

    const { value: formValues, isConfirmed } = await Swal.fire({
        title: '<i class="bi bi-stars da-queue-title-icon"></i> Queue Developer Agent',
        html: `
            <div class="da-swal-body">
                <div class="da-swal-row-2col" id="swal-top-row">
                    <div class="da-swal-field">
                        <label class="swal-label"><i class="bi bi-cpu-fill"></i> Agent Version</label>
                        <select id="swal-agent-version" class="swal-select">
                            ${versionOptions}
                        </select>
                    </div>
                    <div class="da-swal-field" id="swal-project-field">
                        <label class="swal-label"><i class="bi bi-folder2-open"></i> Project</label>
                        <select id="swal-project" class="swal-select" ${isProjectScopedOrg ? 'disabled' : ''}>
                            ${projectOptions}
                        </select>
                    </div>
                </div>
                <div class="da-swal-auto-project" id="swal-auto-project" style="display:none;">
                    <i class="bi bi-folder2-open"></i>
                    <span>Project</span>
                    <strong>${IndexLibrary.escapeHtml(currentProjectContext.name || 'Unavailable')}</strong>
                </div>
                <div class="da-swal-field" id="swal-repos-field">
                    <label class="swal-label">
                        <i class="bi bi-git"></i> Repositories
                        <span class="swal-label-hint">Hold Ctrl / Cmd to select multiple</span>
                    </label>
                    <select id="swal-repos" class="swal-select swal-select-multi" multiple>
                        <option value="" disabled>Select a project first</option>
                    </select>
                </div>
                <div id="swal-repo-roles"></div>
            </div>
        `,
        customClass: {
            popup: 'da-queue-popup',
            title: 'da-queue-title',
            htmlContainer: 'da-queue-html',
            confirmButton: 'da-queue-btn-confirm',
            cancelButton: 'da-queue-btn-cancel',
            actions: 'da-queue-actions',
        },
        showCancelButton: true,
        confirmButtonText: '<i class="bi bi-send-fill"></i> Queue',
        cancelButtonText: 'Cancel',
        buttonsStyling: false,
        focusConfirm: false,
        didOpen: () => {
            const projectSelect = document.getElementById('swal-project');
            const reposSelect = document.getElementById('swal-repos');
            const rolesContainer = document.getElementById('swal-repo-roles');
            const versionSelect = document.getElementById('swal-agent-version');
            const reposField = document.getElementById('swal-repos-field');
            const projectField = document.getElementById('swal-project-field');
            const autoProjectNote = document.getElementById('swal-auto-project');
            const topRow = document.getElementById('swal-top-row');

            makeQueuePopupResizable();

            // v1 (Prototype) uses this work item's project and clones nothing, so it needs
            // neither the project field nor the repo/role fields. Under org scope the project
            // field is absent for v2 too, since repositories span every project.
            function toggleVersionFields() {
                const isV2 = versionSelect.value === 'v2';
                reposField.style.display = isV2 ? '' : 'none';
                rolesContainer.style.display = isV2 ? '' : 'none';
                projectField.style.display = isV2 ? '' : 'none';
                autoProjectNote.style.display = isV2 ? 'none' : '';
                topRow.classList.toggle('is-single', !isV2);
            }
            versionSelect.addEventListener('change', toggleVersionFields);
            toggleVersionFields();

            const roleOptionsHtml = ROLE_OPTIONS.map(r =>
                `<option value="${r}">${r}</option>`
            ).join('');

            function renderRoleRows() {
                const selected = Array.from(reposSelect.selectedOptions);
                if (selected.length === 0) {
                    rolesContainer.innerHTML = '';
                    return;
                }
                rolesContainer.innerHTML = `
                    <div class="da-swal-roles-section">
                        <label class="swal-label"><i class="bi bi-person-badge"></i> Repository Roles</label>
                        <div class="da-swal-roles-list">
                            ${selected.map(opt => `
                                <div class="da-swal-role-row">
                                    <span class="da-repo-chip"><i class="bi bi-git"></i>${IndexLibrary.escapeHtml(opt.text)}</span>
                                    <select class="swal-select swal-role-select da-role-select" data-repo="${IndexLibrary.escapeHtml(opt.text)}">
                                        ${roleOptionsHtml}
                                    </select>
                                </div>
                            `).join('')}
                        </div>
                    </div>`;
            }

            async function loadRepositories(selectedProjectId) {
                reposSelect.innerHTML = '<option value="" disabled>Loading...</option>';
                rolesContainer.innerHTML = '';

                if (!selectedProjectId) {
                    reposSelect.innerHTML = '<option value="" disabled>Select a project first</option>';
                    return;
                }

                try {
                    const repos = sortByName(await IndexLibrary.getReposByProject(selectedProjectId));
                    reposSelect.innerHTML = repos.length === 0
                        ? '<option value="" disabled>No repositories found</option>'
                        : repos.map(r =>
                            `<option value="${IndexLibrary.escapeHtml(r.id)}" data-name="${IndexLibrary.escapeHtml(r.name)}">${IndexLibrary.escapeHtml(r.name)}</option>`
                        ).join('');
                } catch (e) {
                    reposSelect.innerHTML = '<option value="" disabled>Error loading repositories</option>';
                }
            }

            projectSelect.addEventListener('change', function () {
                loadRepositories(this.value);
            });

            reposSelect.addEventListener('change', renderRoleRows);

            // Project scope has exactly one project and it is preselected.
            if (isProjectScopedOrg) {
                loadRepositories(projectSelect.value);
            }
        },
        preConfirm: () => {
            const projectSelect = document.getElementById('swal-project');
            const reposSelect = document.getElementById('swal-repos');
            const agentVersion = document.getElementById('swal-agent-version').value;

            // Prototype runs off the work item itself, so it uses the work item's own project.
            if (agentVersion !== 'v2') {
                return {
                    projectName: currentProjectContext.name,
                    projectId: currentProjectContext.id,
                    selectedRepos: [],
                    repositoryRoles: {},
                    agentVersion
                };
            }

            const selectedProjectOption = projectSelect.options[projectSelect.selectedIndex];
            const projectName = selectedProjectOption ? selectedProjectOption.text : '';
            const projectId = projectSelect.value;

            if (!projectId) {
                Swal.showValidationMessage('Please select a project.');
                return false;
            }

            const selectedRepos = Array.from(reposSelect.selectedOptions).map(opt => ({
                id: opt.value,
                name: opt.text
            }));

            if (selectedRepos.length === 0) {
                Swal.showValidationMessage('Please select at least one repository.');
                return false;
            }

            // Collect roles from the dynamically rendered dropdowns
            const repositoryRoles = {};
            document.querySelectorAll('.swal-role-select').forEach(sel => {
                repositoryRoles[sel.dataset.repo] = sel.value;
            });

            return { projectName, projectId, selectedRepos, repositoryRoles, agentVersion };
        }
    });

    if (!isConfirmed || !formValues) return;

    const { projectName, projectId, selectedRepos, repositoryRoles, agentVersion } = formValues;

    const repoNames = selectedRepos.map(r => r.name);
    await IndexLibrary.QueueDeveloperAgentWorkItem(
        workItemId,
        projectName,
        projectId,
        organizationId,
        organizationName,
        repoNames,
        repositoryRoles,
        agentVersion
    );

    await new Promise(resolve => setTimeout(resolve, 1000));
    await updateDeveloperAgentTableBody();
}