
let userguidelink = '';
let userhelplink = '';

const CONFIG = {


    endpoints: {
        evolve: null,
        groom: null,
        summary: null,
        release: null,
        timesheets: null,
    },

    data: {

        meta: { lastUpdated: '' },

        evolve: {
            kpi: {
                totalSessions: 0,
                workItemsCreated: 0,
                acceptanceRate: '0%',
                acceptanceRatePct: 0,
                acceptanceTrend: '',
                bouncedSessions: 0,
            },
            monthly: [],
            workItemTypes: [],
            avgPerSession: {
                suggestionsShown: 0,
                itemsCreated: 0,
                roundsPerSession: 0,
                reAsks: 0,
            },
            topUsers: [],
        },

        groom: {
            kpi: {
                totalSessions: 0,
                itemsGroomed: 0,
                acceptanceRate: '0%',
                acceptanceRatePct: 0,
                acceptanceTrend: '',
                bouncedSessions: 0,
            },
            backlogCoverage: { groomed: 0, total: 0, pct: 0 },
            monthly: [],
            reGroomed: [],
            typesFunnel: [],
            avgPerSession: {
                suggestionsShown: 0,
                acceptedPerSession: 0,
                roundsPerSession: 0,
                sessionsPerItem: 0,
            },
            crossModule: { createdThenGroomed: 0, pct: 0 },
        },

        summary: {
            kpi: {
                summariesGenerated: 0,
                lastUpdatedWorkItemId: 0,
                lastUpdatedDate: '',
                lastUpdatedWorkItemTitle: '',
                activeUsers: 0,
                reGenerated: 0,
            },
            monthly: [],
            topUsers: [],
            reGeneratedByType: [],
            coverage: { summarised: 0, total: 0, pct: 0, notYet: 0 },
            crossModule: { groomedAndSummarised: 0, pct: 0 },
        },

        release: {
            kpi: { totalReleases: 0, onTrack: 0, atRisk: 0, offTrack: 0 },
            monthly: [],
            avgMetrics: {
                avgItemsPerRelease: 0,
                largestRelease: 0,
                smallestRelease: 0,
                avgDuration: '0d',
            },
            comparison: {
                thisMonth: 0,
                thisMonthDiff: '+0',
                onTrack: '0 (0%)',
                atRisk: '0 (0%)',
                offTrack: '0 (0%)',
                lastMonthTotal: 0,
            },
            acceptance: { evolve: 0, groom: 0, overallPct: 0 },
        },

        timesheets: {
            totalHours: 0,
            billableHours: 0,
            nonBillableHours: 0,
            activeTimerCount: 0,
            workTypeBreakdown: [],
            projectBreakdown: [],
            dailyTrend: [],
            userBreakdown: [],
            topWorkItems: [],
            activeTimers: [],
            ptUserType: null,
        },

    },
};



const WORK_ITEM_TYPE_COLORS = {
    'task': { color: '#d08000', trend: '—', trendColor: '#605e5c' },
    'bug': { color: '#d13438', trend: '—', trendColor: '#605e5c' },
    'feature': { color: '#0078d4', trend: '—', trendColor: '#605e5c' },
    'user story': { color: '#107c10', trend: '—', trendColor: '#605e5c' },
    'epic': { color: '#8764b8', trend: '—', trendColor: '#605e5c' },
    'test case': { color: '#00b7c3', trend: '—', trendColor: '#605e5c' },
    'test suite': { color: '#00b7c3', trend: '—', trendColor: '#605e5c' },
    'test plan': { color: '#00b7c3', trend: '—', trendColor: '#605e5c' },
    'issue': { color: '#d08000', trend: '—', trendColor: '#605e5c' },
    'shared steps': { color: '#605e5c', trend: '—', trendColor: '#605e5c' },
    'shared parameter': { color: '#605e5c', trend: '—', trendColor: '#605e5c' },
};

function getTypeColor(typeName) {
    const key = (typeName || '').trim().toLowerCase();
    return WORK_ITEM_TYPE_COLORS[key] || { color: '#605e5c', trend: '—', trendColor: '#605e5c' };
}


// Shared icon builder — used by both Evolve table and Overview heatmap
function buildTypeIcon(iconUrl, color) {
    if (iconUrl) {
        return `<img src="${safeUrl(iconUrl)}" width="14" height="14"
                     style="border-radius:2px;flex-shrink:0;object-fit:contain;vertical-align:middle" />`;
    }
    return `<div class="wi-dot" style="background:${safeCssColor(color)}"></div>`;
}

function deepMerge(target, source) {
    const result = Object.assign({}, target);
    for (const key of Object.keys(source)) {
        if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
            result[key] = deepMerge(target[key] || {}, source[key]);
        } else {
            result[key] = source[key];
        }
    }
    return result;
}

async function buildHeaders() {
    try {
        const azureAuthenticationHeader = await IndexLibrary.GetAccessToken();
        const projectName = await IndexLibrary.GetCurrentProjectName();
        const projectId = await IndexLibrary.GetCurrentProjectId();
        const accountName = await IndexLibrary.GetAccountNameAsync();
        const accountId = IndexLibrary.GetAccountId();

        return {
            'Authorization': azureAuthenticationHeader,
            'OrganizationName': accountName,
            'OrganizationId': accountId,
            'ProjectName': projectName,
            'ProjectId': projectId,
        };
    } catch (err) {
        console.warn('[dashboard] IndexLibrary not available:', err.message);
        return {};
    }
}

async function fetchModuleData(module, headers) {
    const url = CONFIG.endpoints[module];
    if (!url) return;
    try {
        const res = await IndexLibrary.GetRequest({
            endpoint: url,
            headers
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();

        if (Array.isArray(json.workItemTypes)) {
            json.workItemTypes = json.workItemTypes.map(t => {
                const meta = getTypeColor(t.type);
                return {
                    type: t.type,
                    count: t.count,
                    pct: t.pct,
                    iconUrl: t.iconUrl || null,
                    color: meta.color,
                    trend: meta.trend,
                    trendColor: meta.trendColor,
                };
            });
        }

        CONFIG.data[module] = deepMerge(CONFIG.data[module], json);
    } catch (err) {
        console.warn(`[dashboard] Could not fetch ${module}:`, err.message);
    }
}

async function fetchTimesheetsOverview() {
    try {
        const today    = new Date();
        const fromDate = new Date(today.getFullYear(), today.getMonth(), 1).toISOString().slice(0, 10);
        const toDate   = today.toISOString().slice(0, 10);
        const userEmail = IndexLibrary.GetUserEmail ? IndexLibrary.GetUserEmail() : '';

        const res = await IndexLibrary.PostRequest({
            endpoint: '/api/Timesheet/Dashboard',
            body: { currentUserEmail: userEmail, fromDate, toDate, projectName: '', userEmail: '' }
        });
        if (!res.ok) return;
        const data = await res.json();
        CONFIG.data.timesheets = { ...CONFIG.data.timesheets, ...data, ptUserType: CONFIG.data.timesheets.ptUserType };
    } catch (e) {
        console.warn('[timesheets] Overview fetch failed:', e.message);
    }
}

async function fetchAll() {
    const headers = await buildHeaders();
    await Promise.all([
        ...Object.keys(CONFIG.endpoints).map(m => fetchModuleData(m, headers)),
        fetchTimesheetsOverview()
    ]);
    renderAll();
    hideLoader();
}



function hideLoader() {
    const loader = document.getElementById('insights-loader');
    if (loader) loader.classList.add('hidden');
}

function blockInsights() {
    const content = document.getElementById('insightsContent');
    if (content) content.style.display = 'none';
    hideLoader();
}

async function CreateTrialLicense() {
    await IndexLibrary.CreateTrialLicense(false);
}

function openUserGuideLink() {
    IndexLibrary.openInNewTab(userguidelink);
}

function openUserHelpLink() {
    IndexLibrary.openInNewTab(userhelplink);
}

async function initDashboard() {
    try {
        const config = await IndexLibrary.GetExtensionConfig();

        const base = (config.apiBaseUrl || '').replace(/\/$/, '');
        userguidelink = config.userguidelink || '';
        userhelplink = config.userhelplink || '';

        CONFIG.endpoints.evolve = `/api/Insights/GetEvolveInsights`;
        CONFIG.endpoints.groom = `/api/Insights/GetGroomInsights`;
        CONFIG.endpoints.summary = `/api/Insights/GetSummaryInsights`;
        CONFIG.endpoints.release = `/api/Insights/GetReleaseInsights`;
        CONFIG.endpoints.timesheets = null; // loaded on-demand via loadTimesheetsTab()

        const azureAuthenticationHeader = await IndexLibrary.GetAccessToken();
        const orgName = await IndexLibrary.GetAccountNameAsync();
        const userEmail = IndexLibrary.GetUserEmail();

        const licenseData = { organizationName: orgName };
        let licenseResponse;
        try {
            const licenseRes = await IndexLibrary.PostRequest({
                endpoint: '/api/License/GetLicense',
                body: licenseData
            });

            if (licenseRes.status === 404) {
                document.getElementById('startTrialMessage').style.display = '';
                blockInsights();
                return;
            }
            licenseResponse = await licenseRes.json();
        } catch (err) {
            document.getElementById('startTrialMessage').style.display = '';
            blockInsights();
            return;
        }

        if (licenseResponse.status !== undefined && licenseResponse.status === 404) {
            document.getElementById('startTrialMessage').style.display = '';
            CreateTrialLicense();
            blockInsights();
            return;
        }

        const userLicensedRes = await IndexLibrary.GetRequest({
            endpoint: `/api/License/IsUserLicensed?organizationName=${orgName}&email=${userEmail}&moduleUserType=devOpsAssist`
        });
        const isUserLicensed = await userLicensedRes.text();

        if (isUserLicensed.toLowerCase() === 'false') {
            const authErrEl = document.getElementById('authorizationErrorMessage');
            authErrEl.textContent = 'You are not authorized to perform this action. Please contact your organization administrator.';
            authErrEl.style.display = '';
            blockInsights();
            return;
        }

        if (licenseResponse.licenseType === 0) {
            const trialEl = document.getElementById('startTrialMessage');
            const licenseExpiryDate = new Date(licenseResponse.expiryDateTimeUtc);
            if (licenseExpiryDate < new Date()) {
                trialEl.textContent = 'Your license is invalid or expired. Please contact your organization administrator.';
                trialEl.style.display = '';
                blockInsights();
                return;
            } else {
                trialEl.innerHTML = `Devops Assist AI trial license is valid until ${licenseExpiryDate.toLocaleDateString('en-US')}`;
                trialEl.style.display = '';
            }
        }

        await fetchAll();
    } catch (err) {
        console.warn('[dashboard] Could not load config.json:', err.message);
        hideLoader();
    }
}




function buildGroupedBarChart(months, segmentsFn) {
    if (!months.length) return '<div class="chart-no-data">No data</div>';
    const maxVal = Math.max(...months.flatMap(r => segmentsFn(r).map(s => s.value)));
    const scale = maxVal > 0 ? 100 / maxVal : 1;

    return months.map((row, mi) => {
        const segments = segmentsFn(row);
        const bars = segments.map((s, si) => {
            const h = Math.round(s.value * scale);
            const delay = (mi * 120 + si * 60) + 'ms';
            return `
            <div class="grouped-bar-wrap">
                <div class="grouped-bar-val">${escapeHtml(s.value)}</div>
                <div class="grouped-bar bar-seg--anim"
                    style="--bar-h:${h}px; background:${safeCssColor(s.color)}; animation-delay:${delay}; border-radius:2px 2px 0 0;">
                    <div class="bar-tooltip">${escapeHtml(s.label)}: <strong>${escapeHtml(s.value)}</strong></div>
                </div>
            </div>`;
        }).join('');

        return `
        <div class="grouped-col">
            <div class="grouped-bars">${bars}</div>
            <div class="grouped-tick">${escapeHtml(row.month)}</div>
        </div>`;
    }).join('');
}

function buildBarChart(months, segmentsFn) {
    if (!months.length) return '<div class="chart-no-data">No data</div>';
    const maxVal = Math.max(...months.map(r => segmentsFn(r).reduce((a, s) => a + s.height, 0)));
    const scale = maxVal > 0 ? 100 / maxVal : 1;

    return months.map((row, mi) => {
        const segments = segmentsFn(row);
        const total = segments.reduce((a, s) => a + s.height, 0);
        const segs = segments.map((s, si) => {
            const h = Math.round(s.height * scale);
            const delay = (mi * 100 + si * 40) + 'ms';
            return `<div
                class="bar-seg bar-seg--anim"
                style="--bar-h:${h}px; background:${safeCssColor(s.color)}; animation-delay:${delay};">
                <div class="bar-tooltip">${escapeHtml(s.label || '')}: <strong>${escapeHtml(s.height)}</strong></div>
            </div>`;
        }).join('');

        return `
        <div class="bar-col">
            <div class="bar-val-lbl">${escapeHtml(total)}</div>
            <div class="bar-stacks">${segs}</div>
            <div class="bar-tick">${escapeHtml(row.month)}</div>
        </div>`;
    }).join('');
}

function buildDonut(segments, labelTop, labelBottom, r = 32, cx = 45, cy = 45, sw = 13) {
    const circ = 2 * Math.PI * r;
    let offset = 0;
    const arcs = segments.map(s => {
        const dash = (s.pct / 100) * circ;
        const arc = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${safeCssColor(s.color)}"
            stroke-width="${sw}" stroke-dasharray="${dash.toFixed(1)} ${(circ - dash).toFixed(1)}"
            stroke-dashoffset="${-offset.toFixed(1)}" transform="rotate(-90 ${cx} ${cy})"/>`;
        offset += dash;
        return arc;
    });
    const size = cx * 2;
    return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="flex-shrink:0">
        <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#f3f2f1" stroke-width="${sw}"/>
        ${arcs.join('')}
        <text x="${cx}" y="${cy - 3}" text-anchor="middle" font-size="14" font-weight="500" fill="#323130">${escapeHtml(labelTop)}</text>
        <text x="${cx}" y="${cy + 9}" text-anchor="middle" font-size="9" fill="#605e5c">${escapeHtml(labelBottom)}</text>
    </svg>`;
}

function buildStatList(items) {
    return items.map(i =>
        `<div class="stat-row"><span>${escapeHtml(i.name)}</span><span style="font-weight:500">${escapeHtml(i.count)}</span></div>`
    ).join('');
}

function buildFunnel(items, bgColor, textColor) {
    return items.map(i => `
    <div class="funnel-row">
        <div class="funnel-label">${escapeHtml(i.type)}</div>
        <div class="funnel-bar-wrap">
            <div class="funnel-bar" style="width:${Number(i.widthPct) || 0}%;background:${safeCssColor(bgColor)};color:${safeCssColor(textColor)}">${escapeHtml(i.count)}</div>
        </div>
    </div>`).join('');
}



function renderEvolve() {
    const d = CONFIG.data.evolve;

    el('ev-total-sessions').textContent = d.kpi.totalSessions;
    el('ev-items-created').textContent = d.kpi.workItemsCreated;
    el('ev-acceptance-rate').textContent = d.kpi.acceptanceRate;
    el('ev-acceptance-trend').textContent = d.kpi.acceptanceTrend;
    el('ev-bounced-sessions').textContent = d.kpi.bouncedSessions;

    el('ev-monthly-chart').innerHTML = buildGroupedBarChart(d.monthly, row => [
        { value: row.total, color: '#0078d4', label: 'Total' },
        { value: row.accepted, color: '#107c10', label: 'Accepted' },
        { value: row.bounced, color: '#d13438', label: 'Bounced' },
    ]);

    const bounced = 100 - d.kpi.acceptanceRatePct;
    el('ev-donut').innerHTML = buildDonut(
        [{ pct: d.kpi.acceptanceRatePct, color: '#107c10' }, { pct: bounced, color: '#d13438' }],
        d.kpi.acceptanceRate, 'accepted'
    );
    el('ev-donut-accepted').textContent = `Accepted ${d.kpi.acceptanceRate}`;
    el('ev-donut-bounced').textContent = `Bounced ${bounced}%`;
    el('ev-wi-types').innerHTML = d.workItemTypes.map(t => `
    <tr>
        <td><div class="wi-type-cell">${buildTypeIcon(t.iconUrl, t.color)}${escapeHtml(t.type)}</div></td>
        <td>${escapeHtml(t.count)}</td>
        <td>${escapeHtml(t.pct)}</td>
    </tr>`
    ).join('');

    const avg = d.avgPerSession;
    el('ev-avg-suggestions').textContent = avg.suggestionsShown;
    el('ev-avg-items').textContent = avg.itemsCreated;
    el('ev-avg-rounds').textContent = avg.roundsPerSession;
    el('ev-avg-reasks').textContent = avg.reAsks;

    // el('ev-top-users').innerHTML = buildStatList(d.topUsers);
}

function renderGroom() {
    const d = CONFIG.data.groom;

    el('gr-total-sessions').textContent = d.kpi.totalSessions;
    el('gr-items-groomed').textContent = d.kpi.itemsGroomed;
    el('gr-acceptance-rate').textContent = d.kpi.acceptanceRate;
    el('gr-acceptance-trend').textContent = d.kpi.acceptanceTrend;
    el('gr-bounced').textContent = d.kpi.bouncedSessions;

    const bc = d.backlogCoverage;
    el('gr-coverage-pct').textContent = bc.pct + '%';
    el('gr-coverage-sub').textContent = `${bc.groomed} of ${bc.total} work items`;
    el('gr-coverage-bar').style.width = bc.pct + '%';

    el('gr-monthly-chart').innerHTML = buildGroupedBarChart(d.monthly, row => [
        { value: row.total, color: '#8764b8', label: 'Total' },
        { value: row.accepted, color: '#107c10', label: 'Accepted' },
        { value: row.bounced, color: '#d13438', label: 'Bounced' },
    ]);

    el('gr-regroomed').innerHTML = d.reGroomed.map(r =>
        `<div class="stat-row">
            <span>${r.id} — ${r.name}</span>
            <span class="badge badge-blue">${r.times}</span>
        </div>`).join('');

    el('gr-types-funnel').innerHTML = buildFunnel(d.typesFunnel, '#f4f0fb', '#3b1f6e');

    const avg = d.avgPerSession;
    el('gr-avg-shown').textContent = avg.suggestionsShown;
    el('gr-avg-accepted').textContent = avg.acceptedPerSession;
    el('gr-avg-rounds').textContent = avg.roundsPerSession;
    el('gr-avg-sessions-per-item').textContent = avg.sessionsPerItem;

    const cm = d.crossModule;
    el('gr-cross-count').textContent = cm.createdThenGroomed;
    el('gr-cross-bar').style.width = cm.pct + '%';
    el('gr-cross-sub').textContent = `${cm.pct}% of AI-created items later received an AI description`;
}



function renderSummary() {
    const d = CONFIG.data.summary;

    el('su-generated').textContent = d.kpi.summariesGenerated;
    if (d.kpi.lastUpdatedWorkItemId) {
        el('su-unique').innerHTML = `
        <div class="kpi-last-summarised-title">
            #${escapeHtml(d.kpi.lastUpdatedWorkItemId)} · ${escapeHtml(d.kpi.lastUpdatedWorkItemTitle || '')}
        </div>
        <div class="kpi-last-summarised-date">${escapeHtml(d.kpi.lastUpdatedDate || '')}</div>`;
    } else {
        el('su-unique').textContent = '—';
    }
    el('su-users').textContent = d.kpi.activeUsers;
    el('su-regen').textContent = d.kpi.reGenerated;

    const maxCount = d.monthly.length ? Math.max(...d.monthly.map(r => r.count)) : 0;
    el('su-monthly-chart').innerHTML = d.monthly.length ? d.monthly.map((row, i) => {
        const h = maxCount > 0 ? Math.round((row.count / maxCount) * 100) : 0;
        const delay = (i * 80) + 'ms';
        return `<div class="bar-col">
            <div class="bar-val-lbl">${escapeHtml(row.count)}</div>
            <div class="bar-stacks">
                <div class="bar-seg bar-seg--anim"
                    style="--bar-h:${h}px; background:#00b7c3; border-radius:2px 2px 0 0; animation-delay:${delay};">
                    <div class="bar-tooltip">Summaries: <strong>${escapeHtml(row.count)}</strong></div>
                </div>
            </div>
            <div class="bar-tick">${escapeHtml(row.month)}</div>
        </div>`;
    }).join('') : '<div class="chart-no-data">No data</div>';

    // el('su-top-users').innerHTML = buildStatList(d.topUsers);
    el('su-regen-types').innerHTML = buildFunnel(d.reGeneratedByType, '#d1f8fb', '#004f55');

    const cov = d.coverage;
    el('su-not-yet').textContent = cov.notYet;
    el('su-coverage-bar').style.width = cov.pct + '%';
    el('su-coverage-sub').textContent = `${cov.summarised} of ${cov.total} items summarised (${cov.pct}% coverage)`;

    const cm = d.crossModule;
    el('su-cross-count').textContent = cm.groomedAndSummarised;
    el('su-cross-bar').style.width = cm.pct + '%';
    el('su-cross-sub').textContent = `${cm.pct}% of groomed items also have a summary`;
}


function renderRelease() {
    const d = CONFIG.data.release;

    el('re-total').textContent = d.kpi.totalReleases;
    el('re-on-track').textContent = d.kpi.onTrack;
    el('re-at-risk').textContent = d.kpi.atRisk;
    el('re-off-track').textContent = d.kpi.offTrack;

    el('re-monthly-chart').innerHTML = buildGroupedBarChart(d.monthly, row => [
        { value: row.onTrack, color: '#107c10', label: 'On track' },
        { value: row.atRisk, color: '#d08000', label: 'At risk' },
        { value: row.offTrack, color: '#d13438', label: 'Off track' },
    ]);

    const total = d.kpi.totalReleases;
    const onPct = total ? Math.round(d.kpi.onTrack / total * 100) : 0;
    const riskPct = total ? Math.round(d.kpi.atRisk / total * 100) : 0;
    const offPct = total ? Math.round(d.kpi.offTrack / total * 100) : 0;
    el('re-donut').innerHTML = buildDonut(
        [
            { pct: onPct, color: '#107c10' },
            { pct: riskPct, color: '#d08000' },
            { pct: offPct, color: '#d13438' },
        ],
        onPct + '%', 'on track', 36, 50, 50, 14
    );
    el('re-donut-legend').innerHTML = `
    <div class="dl-item"><div class="legend-sq" style="background:#107c10"></div>On track ${onPct}%</div>
    <div class="dl-item"><div class="legend-sq" style="background:#d08000"></div>At risk ${riskPct}%</div>
    <div class="dl-item"><div class="legend-sq" style="background:#d13438"></div>Off track ${offPct}%</div>`;

    const avg = d.avgMetrics;
    el('re-avg-items').textContent = avg.avgItemsPerRelease;
    el('re-avg-largest').textContent = avg.largestRelease;
    el('re-avg-smallest').textContent = avg.smallestRelease;
    el('re-avg-duration').textContent = avg.avgDuration;

    const cmp = d.comparison;
    el('re-cmp-this').innerHTML = `${escapeHtml(cmp.thisMonth)} <span style="font-size:10px;color:#107c10">${escapeHtml(cmp.thisMonthDiff)}</span>`;
    el('re-cmp-on-track').textContent = cmp.onTrack;
    el('re-cmp-at-risk').textContent = cmp.atRisk;
    el('re-cmp-off-track').textContent = cmp.offTrack;
    el('re-cmp-last').textContent = cmp.lastMonthTotal;
}

function renderOverview() {
    const e = CONFIG.data.evolve;
    const g = CONFIG.data.groom;
    const s = CONFIG.data.summary;
    const r = CONFIG.data.release;

    // ── Module KPI cards ─────────────────────────────────────
    el('ov-ev-sessions').textContent = e.kpi.totalSessions;
    el('ov-ev-items').textContent = e.kpi.workItemsCreated;
    el('ov-gr-groomed').textContent = g.kpi.itemsGroomed;
    el('ov-gr-rate').textContent = g.kpi.acceptanceRate;
    el('ov-su-generated').textContent = s.kpi.summariesGenerated;
    el('ov-su-coverage').textContent = s.coverage.pct + '%';
    el('ov-re-releases').textContent = r.kpi.totalReleases;
    el('ov-re-ontrack').textContent = r.kpi.onTrack;

    // ── Timesheets Overview card ──────────────────────────────
    renderTimesheetsOverviewCard();

    // ── Total AI interactions ─────────────────────────────────
    const totalInteractions = e.kpi.totalSessions + g.kpi.totalSessions + s.kpi.summariesGenerated;
    el('ov-total-interactions').textContent = totalInteractions;
    el('ov-interactions-breakdown').textContent =
        `Evolve ${e.kpi.totalSessions} · Groom ${g.kpi.totalSessions} · Summary ${s.kpi.summariesGenerated}`;

    // ── AI acceptance across modules (from release endpoint) ──
    const acc = r.acceptance;
    el('ov-acc-evolve-bar').style.width = acc.evolve + '%';
    el('ov-acc-evolve-val').textContent = acc.evolve + '%';
    el('ov-acc-groom-bar').style.width = acc.groom + '%';
    el('ov-acc-groom-val').textContent = acc.groom + '%';
    // Reverse-calculate accepted counts from percentages
    const evolveAcc = Math.round(acc.evolve / 100 * e.kpi.totalSessions);
    const groomAcc = Math.round(acc.groom / 100 * g.kpi.totalSessions);

    // Summary: use summarised items / total backlog (coverage) as its acceptance metric
    const summaryAcc = s.coverage.summarised;
    const summaryTotal = s.coverage.total;

    const overallTotal = e.kpi.totalSessions + g.kpi.totalSessions + summaryTotal;
    const overallAccepted = evolveAcc + groomAcc + summaryAcc;
    const overallPct = overallTotal > 0
        ? Math.round(overallAccepted / overallTotal * 100)
        : 0;

    el('ov-acc-overall-bar').style.width = overallPct + '%';
    el('ov-acc-overall-val').textContent = overallPct + '%';
    // Summary acceptance: prefer server-provided value, fall back to summary coverage percent
    const summaryAcceptance = (acc && typeof acc.summary !== 'undefined') ? acc.summary : (s.coverage && typeof s.coverage.pct !== 'undefined' ? s.coverage.pct : 0);
    if (document.getElementById('ov-acc-summary-bar')) {
        el('ov-acc-summary-bar').style.width = summaryAcceptance + '%';
    }
    if (document.getElementById('ov-acc-summary-val')) {
        el('ov-acc-summary-val').textContent = summaryAcceptance + '%';
    }


    const evolveMap = Object.fromEntries((e.workItemTypes || []).map(t => [t.type.toLowerCase(), t.count]));
    const groomMap = Object.fromEntries((g.typesFunnel || []).map(t => [t.type.toLowerCase(), t.count]));
    const summaryMap = Object.fromEntries((s.reGeneratedByType || []).map(t => [t.type.toLowerCase(), t.count]));

    // Build icon map from ALL 3 modules — so types only in Summary/Groom still get icons
    const iconMap = {};
    (e.workItemTypes || []).forEach(t => { if (t.iconUrl) iconMap[t.type.toLowerCase()] = t.iconUrl; });
    (g.typesFunnel || []).forEach(t => { if (t.iconUrl) iconMap[t.type.toLowerCase()] = t.iconUrl; });
    (s.reGeneratedByType || []).forEach(t => { if (t.iconUrl) iconMap[t.type.toLowerCase()] = t.iconUrl; });
    // Union of all types across all three modules
    const allTypes = [...new Set([
        ...(e.workItemTypes || []).map(t => t.type),
        ...(g.typesFunnel || []).map(t => t.type),
        ...(s.reGeneratedByType || []).map(t => t.type),
    ])];


    function hmClass(n) {
        if (!n || n === 0) return 'hm-zero';
        if (n >= 30) return 'hm-high';
        if (n >= 10) return 'hm-mid';
        return 'hm-low';
    }

    el('ov-heatmap').innerHTML = allTypes.length ? allTypes.map(type => {
        const key = type.toLowerCase();
        const meta = getTypeColor(type);
        const color = meta.color || '#605e5c';
        const ev = evolveMap[key] || 0;
        const gr = groomMap[key] || 0;
        const su = summaryMap[key] || 0;
        const iconUrl = iconMap[key] || null;
        const iconHtml = buildTypeIcon(iconUrl, color);
        return `<tr>
            <td><div class="wi-type-cell">${iconHtml}${escapeHtml(type)}</div></td>
            <td><div class="hm-cell ${hmClass(ev)}">${escapeHtml(ev || '—')}</div></td>
            <td><div class="hm-cell ${hmClass(gr)}">${escapeHtml(gr || '—')}</div></td>
            <td><div class="hm-cell ${hmClass(su)}">${escapeHtml(su || '—')}</div></td>
        </tr>`;
    }).join('') : `<tr><td colspan="4" style="padding:12px 8px;color:var(--text-muted);font-size:12px">No data available</td></tr>`;
}



function renderAll() {
    renderOverview();
    renderEvolve();
    renderGroom();
    renderSummary();
    renderRelease();
    // Timesheets loaded on demand when tab is first opened
}

function el(id) { return document.getElementById(id); }

function escapeHtml(value) {
    return String(value ?? '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

function escapeJsArg(value) {
    return escapeHtml(JSON.stringify(String(value ?? '')));
}

function safeUrl(url) {
    const value = String(url ?? '').trim();
    return /^(https?:|data:image\/)/i.test(value) ? escapeHtml(value) : '';
}

function safeCssColor(value) {
    const color = String(value ?? '').trim();
    return /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%]+\)|hsla?\([\d\s.,%]+\)|[a-z]+)$/i.test(color) ? color : '';
}

function switchTab(name) {
    document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
    document.querySelectorAll('.subnav-tab').forEach(t => t.classList.remove('active'));
    const sec = document.getElementById('sec-' + name);
    if (sec) sec.classList.add('active');
    const tabIndex = { overview: 0, evolve: 1, groom: 2, summary: 3, release: 4, timesheets: 5 };
    const tabs = document.querySelectorAll('.subnav-tab');
    if (tabs[tabIndex[name]]) tabs[tabIndex[name]].classList.add('active');
    if (name === 'timesheets') loadTimesheetsTab();
}

document.addEventListener('DOMContentLoaded', () => {
    const tabNames = ['overview', 'evolve', 'groom', 'summary', 'release', 'timesheets'];
    document.querySelectorAll('.subnav-tab').forEach((tab, i) => {
        tab.addEventListener('click', () => switchTab(tabNames[i]));
    });

    switchTab('overview');
    renderAll();

    initDashboard();
});

/* ════════════════════════════════════════════════════════════
   TIMESHEETS — data, state, helpers
   ════════════════════════════════════════════════════════════ */

const PT_CHART_COLORS = ['#0078d4','#8764b8','#107c10','#d08000','#d13438','#00b7c3','#6b21a8','#b45309','#1d4ed8','#be185d'];
const PT_OTHER_COLOR  = '#c8c6c4';
const PT_BAR_COLORS   = ['#0078d4','#8764b8','#107c10','#d08000','#d13438','#00b7c3','#6b21a8','#b45309'];
const PT_USER_PAGE    = 5;
const PT_TIMER_PAGE   = 5;

let ptChartWT    = null;   // unused — kept for legacy compat
let ptChartProj  = null;
let ptChartTrend = null;
let ptChartUsers = null;
let ptUserExpanded   = false;
let ptTimerExpanded  = false;
let ptTabLoaded      = false;
let ptStartTimes     = {};
let ptTimerInterval  = null;

function fmtPtHours(decHours) {
    if (typeof decHours !== 'number' || isNaN(decHours)) return '—';
    const h = Math.floor(decHours);
    const m = Math.round((decHours - h) * 60);
    return `${h}:${String(m).padStart(2, '0')}`;
}

function ptCapWithOther(items, max, labelKey, hoursKey) {
    if (items.length <= max) {
        return items.map((x, i) => ({ label: x[labelKey], hours: x[hoursKey], color: x.color || PT_CHART_COLORS[i % PT_CHART_COLORS.length] }));
    }
    const sorted = [...items].sort((a, b) => b[hoursKey] - a[hoursKey]);
    const top  = sorted.slice(0, max);
    const rest = sorted.slice(max);
    return [
        ...top.map((x, i) => ({ label: x[labelKey], hours: x[hoursKey], color: x.color || PT_CHART_COLORS[i % PT_CHART_COLORS.length] })),
        { label: `Other (${rest.length})`, hours: rest.reduce((s, x) => s + x[hoursKey], 0), color: PT_OTHER_COLOR }
    ];
}

function ptCalcDateRange() {
    const p = document.getElementById('pt-period-sel') ? document.getElementById('pt-period-sel').value : 'month';
    const today = new Date();
    const pad = d => d.toISOString().slice(0, 10);
    switch (p) {
        case 'today':     return { from: pad(today), to: pad(today) };
        case 'week':      { const d = new Date(today); d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); return { from: pad(d), to: pad(today) }; }
        case 'lastmonth': { const f = new Date(today.getFullYear(), today.getMonth() - 1, 1); const t = new Date(today.getFullYear(), today.getMonth(), 0); return { from: pad(f), to: pad(t) }; }
        case '30days':    { const d = new Date(today); d.setDate(d.getDate() - 30); return { from: pad(d), to: pad(today) }; }
        case 'custom':    return { from: document.getElementById('pt-date-from').value, to: document.getElementById('pt-date-to').value };
        default:          return { from: pad(new Date(today.getFullYear(), today.getMonth(), 1)), to: pad(today) };
    }
}

function onPtPeriodChange(v) {
    const cr = document.getElementById('pt-custom-range');
    if (cr) cr.style.display = v === 'custom' ? 'flex' : 'none';
    if (v !== 'custom') loadTimesheetsTab();
}

function onPtCustomDateChange() {
    const from = document.getElementById('pt-date-from').value;
    const to   = document.getElementById('pt-date-to').value;
    if (from && to && from <= to) loadTimesheetsTab();
}

/* ── Overview card ── */
function renderTimesheetsOverviewCard() {
    const d = CONFIG.data.timesheets;
    const hoursEl  = document.getElementById('ov-pt-hours');
    const billEl   = document.getElementById('ov-pt-billable-pct');
    const activeEl = document.getElementById('ov-pt-active');
    if (!hoursEl) return;
    // Only show dashes when no data has arrived yet (totalHours undefined means fetch never ran)
    if (d.totalHours === undefined) {
        hoursEl.textContent  = '—';
        billEl.textContent   = '—';
        activeEl.textContent = '—';
        return;
    }
    hoursEl.textContent  = fmtPtHours(d.totalHours);
    const billPct = d.totalHours > 0 ? ((d.billableHours / d.totalHours) * 100).toFixed(1) + '%' : '—';
    billEl.textContent   = billPct;
    activeEl.textContent = String(d.activeTimerCount);
}

/* ── Load / refresh ── */
async function loadTimesheetsTab() {
    ptTabLoaded = true;
    const range = ptCalcDateRange();

    // Resolve ptUserType from license data if not yet done
    let ptUserType = CONFIG.data.timesheets.ptUserType;
    if (ptUserType === null) {
        try {
            const orgName   = await IndexLibrary.GetAccountNameAsync();
            const userEmail = IndexLibrary.GetUserEmail();
            const licRes    = await IndexLibrary.GetRequest({ endpoint: `/api/License/GetUserLicense?organizationName=${encodeURIComponent(orgName)}&email=${encodeURIComponent(userEmail)}` });
            if (licRes.ok) {
                const lic = await licRes.json();
                ptUserType = typeof lic.ptUserType !== 'undefined' ? lic.ptUserType : null;
            }
        } catch (e) {
            console.warn('[timesheets] Could not resolve ptUserType:', e.message);
        }
        CONFIG.data.timesheets.ptUserType = ptUserType;
    }

    // Apply manager visibility
    const isManager = ptUserType === 1;
    document.querySelectorAll('.pt-manager-only').forEach(el => el.style.display = isManager ? '' : 'none');
    document.querySelectorAll('.pt-manager-panel').forEach(el => el.style.display = isManager ? 'block' : 'none');
    if (document.getElementById('pt-user-col')) document.getElementById('pt-user-col').style.display = isManager ? '' : 'none';

    // Populate project picker on first load
    const projectSel = document.getElementById('pt-project-sel');
    if (projectSel && projectSel.options.length <= 1) {
        try {
            const projRes = await IndexLibrary.GetRequest({ endpoint: '/api/Timesheet/GetProjectNames' });
            if (projRes.ok) {
                const projects = await projRes.json();
                const currentVal = projectSel.value;
                projects.forEach(name => {
                    if (name) {
                        const opt = document.createElement('option');
                        opt.value = name;
                        opt.textContent = name;
                        projectSel.appendChild(opt);
                    }
                });
                projectSel.value = currentVal;
            }
        } catch (e) {
            console.warn('[timesheets] Could not load project names:', e.message);
        }
    }

    // Build request body
    const orgName    = await IndexLibrary.GetAccountNameAsync();
    const userEmail  = IndexLibrary.GetUserEmail();
    const userSel    = document.getElementById('pt-user-sel');
    const body = {
        currentUserEmail: userEmail,
        fromDate:    range.from,
        toDate:      range.to,
        projectName: projectSel ? projectSel.value : '',
        userEmail:   (isManager && userSel) ? userSel.value : '',
    };

    try {
        const res = await IndexLibrary.PostRequest({ endpoint: '/api/Timesheet/Dashboard', body });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json();
        CONFIG.data.timesheets = { ...CONFIG.data.timesheets, ...data, ptUserType };
    } catch (e) {
        console.warn('[timesheets] Dashboard fetch failed:', e.message);
        // Render with whatever data we have (may be empty)
    }

    renderTimesheets();
    renderTimesheetsOverviewCard();
}

/* ── Main render ── */
function renderTimesheets() {
    const d = CONFIG.data.timesheets;

    // KPI cards
    el('pt-kpi-total').textContent    = fmtPtHours(d.totalHours);
    el('pt-kpi-billable').textContent = fmtPtHours(d.billableHours);
    el('pt-kpi-nonbill').textContent  = fmtPtHours(d.nonBillableHours);
    el('pt-kpi-active').textContent   = String(d.activeTimerCount);
    const billSub = document.getElementById('pt-kpi-billable-sub');
    if (billSub) billSub.textContent  = d.totalHours > 0 ? ((d.billableHours / d.totalHours) * 100).toFixed(1) + '% of total' : '';

    ptRenderWorkTypeChart(d.workTypeBreakdown);
    ptRenderProjectChart(d.projectBreakdown);
    ptRenderTrendChart(d.dailyTrend);
    if (CONFIG.data.timesheets.ptUserType === 1) ptRenderUserChart(d.userBreakdown);
    ptRenderTopWI(d.topWorkItems);
    ptRenderActiveTimers(d.activeTimers);
}

/* ── Work type donut — uses existing buildDonut helper ── */
function ptRenderWorkTypeChart(raw) {
    const container = document.getElementById('pt-chart-wt');
    const leg       = document.getElementById('pt-wt-legend');
    if (!container) return;
    const items = (raw || []).slice(0, 10);
    const total = items.reduce((s, w) => s + (w.totalHours || 0), 0);
    if (!items.length || total === 0) {
        container.innerHTML = '<div class="chart-no-data">No data</div>';
        if (leg) leg.innerHTML = '';
        return;
    }
    const segments = items.map(w => ({ color: w.color || '#a19f9d', pct: (w.totalHours / total) * 100 }));
    container.innerHTML = buildDonut(segments, fmtPtHours(total), 'total', 36, 48, 48, 14);
    if (leg) leg.innerHTML = items.map(w =>
        `<div class="pt-wt-legend-row">
            <span class="pt-wt-dot" style="background:${safeCssColor(w.color) || '#a19f9d'}"></span>
            <span class="pt-wt-legend-label">${escapeHtml(w.workTypeName || 'Not Specified')}</span>
            <span class="pt-wt-legend-val">${escapeHtml(fmtPtHours(w.totalHours))}</span>
        </div>`).join('');
}

/* ── Project bar chart — uses funnel-style bars ── */
function ptRenderProjectChart(raw) {
    const wrap = document.getElementById('pt-proj-chart-wrap');
    if (!wrap) return;
    const items = (raw || []).slice(0, 10).sort((a, b) => (b.totalHours || 0) - (a.totalHours || 0));
    if (!items.length) { wrap.innerHTML = '<div class="chart-no-data" style="padding:8px 0">No data</div>'; return; }
    const max = Math.max(...items.map(p => p.totalHours || 0));
    wrap.innerHTML = items.map((p, i) => {
        const pct = max > 0 ? Math.round((p.totalHours || 0) / max * 100) : 0;
        return `<div class="funnel-row">
            <div class="funnel-label" title="${escapeHtml(p.projectName || '')}">${escapeHtml(p.projectName || '—')}</div>
            <div class="funnel-bar-wrap">
                <div class="funnel-bar" style="width:${pct}%;background:${PT_CHART_COLORS[i % PT_CHART_COLORS.length]};color:#fff;min-width:32px">${escapeHtml(fmtPtHours(p.totalHours))}</div>
            </div>
        </div>`;
    }).join('');
}

/* ── Daily trend — uses existing buildBarChart helper ── */
function ptRenderTrendChart(raw) {
    const container = document.getElementById('pt-chart-trend');
    if (!container) return;
    const items = raw || [];
    if (!items.length) { container.innerHTML = '<div class="chart-no-data">No data</div>'; return; }
    const months = items.map(d => ({
        month: new Date(d.date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }),
        hours: d.totalHours || 0
    }));
    container.innerHTML = buildBarChart(months, row => [{
        height: row.hours,
        color: row.hours === 0 ? '#edebe9' : '#ca8a00',
        label: row.month
    }]);
}

/* ── Per-user breakdown — funnel-style bars ── */
function ptRenderUserChart(raw) {
    const wrap = document.getElementById('pt-user-chart-wrap');
    const moreRow = document.getElementById('pt-user-more-btn');
    if (!wrap) return;
    const all     = (raw || []).sort((a, b) => (b.totalHours || 0) - (a.totalHours || 0));
    const visible = ptUserExpanded ? all : all.slice(0, PT_USER_PAGE);
    const hasMore = all.length > PT_USER_PAGE;
    if (!visible.length) { wrap.innerHTML = '<div class="chart-no-data" style="padding:8px 0">No data</div>'; return; }
    const max = Math.max(...visible.map(u => u.totalHours || 0));
    wrap.innerHTML = visible.map((u, i) => {
        const pct = max > 0 ? Math.round((u.totalHours || 0) / max * 100) : 0;
        return `<div class="funnel-row">
            <div class="funnel-label" title="${escapeHtml(u.userEmail || '')}" style="width:140px">${escapeHtml((u.userEmail || '').split('@')[0])}</div>
            <div class="funnel-bar-wrap">
                <div class="funnel-bar" style="width:${pct}%;background:${PT_BAR_COLORS[i % PT_BAR_COLORS.length]};color:#fff;min-width:40px">${escapeHtml(fmtPtHours(u.totalHours))}</div>
            </div>
        </div>`;
    }).join('');
    if (moreRow) {
        moreRow.style.display = hasMore ? '' : 'none';
        const btn = moreRow.querySelector('button');
        if (btn) btn.textContent = ptUserExpanded ? 'Show less' : `Show ${all.length - PT_USER_PAGE} more members`;
    }
}
function togglePtUserChart() {
    ptUserExpanded = !ptUserExpanded;
    ptRenderUserChart(CONFIG.data.timesheets.userBreakdown);
}

/* ── Top work items table ── */
function ptRenderTopWI(raw) {
    const tbody = document.getElementById('pt-top-wi-body');
    if (!tbody) return;
    const items     = raw || [];
    const isManager = CONFIG.data.timesheets.ptUserType === 1;
    if (!items.length) { tbody.innerHTML = '<tr><td colspan="5" style="padding:12px;color:var(--text-muted);font-size:12px">No data for the selected period</td></tr>'; return; }
    tbody.innerHTML = items.map(r => `
        <tr>
            <td><a href="#" style="color:var(--ado-blue);text-decoration:none;font-weight:600">#${escapeHtml(r.workItemId)}</a> ${r.workItemTitle ? escapeHtml(r.workItemTitle) : '<span style="color:var(--text-muted)">Title unavailable</span>'}</td>
            <td style="font-weight:600">${escapeHtml(fmtPtHours(r.totalHours))}</td>
            <td>${escapeHtml(r.workTypeName || 'Not Specified')}</td>
            <td><span class="${r.isBillable ? 'pt-badge-billable' : 'pt-badge-nonbillable'}">${r.isBillable ? 'Billable' : 'Non-billable'}</span></td>
            ${isManager ? `<td style="color:var(--text-secondary);font-size:11px">${escapeHtml(r.topUserEmail || '')}</td>` : ''}
        </tr>`).join('');
}

/* ── Active timers ── */
function ptRenderActiveTimers(raw) {
    const container = document.getElementById('pt-active-timers');
    if (!container) return;
    const all   = raw || [];
    const isOwn = CONFIG.data.timesheets.ptUserType !== 1;
    if (!all.length) { container.innerHTML = '<div style="padding:20px 16px;text-align:center;color:var(--text-muted);font-size:12px">No active timers</div>'; return; }
    all.forEach(e => { if (!ptStartTimes[e.userEmail]) ptStartTimes[e.userEmail] = new Date(e.workStarted).getTime(); });
    const visible = ptTimerExpanded ? all : all.slice(0, PT_TIMER_PAGE);
    const hasMore = all.length > PT_TIMER_PAGE;
    const initials = email => (email || '??').split('@')[0].slice(0, 2).toUpperCase();
    let html = visible.map(e => `
        <div class="pt-timer-row">
            <div class="pt-timer-avatar">${escapeHtml(initials(e.userEmail))}</div>
            <div class="pt-timer-info">
                <div class="pt-timer-user">${escapeHtml(e.userEmail)}</div>
                <div class="pt-timer-wi">#${escapeHtml(e.workItemId)} ${escapeHtml(e.workItemTitle || '')}</div>
            </div>
            <div class="pt-timer-elapsed ${isOwn ? 'own' : ''}" id="pt-elapsed-${escapeHtml(e.timesheetId)}">—</div>
            ${isOwn ? `<button class="pt-stop-btn" onclick="ptStopTimer(${Number(e.timesheetId) || 0},${escapeJsArg(e.userEmail)})">&#9646; Stop</button>` : ''}
        </div>`).join('');
    if (hasMore) html += `<div style="border-top:1px solid var(--border)"><button class="pt-more-btn" onclick="togglePtTimers()">${ptTimerExpanded ? 'Show less' : `Show ${all.length - PT_TIMER_PAGE} more`}</button></div>`;
    container.innerHTML = html;
    ptTickTimers();
    if (ptTimerInterval) clearInterval(ptTimerInterval);
    ptTimerInterval = setInterval(ptTickTimers, 1000);
    document.addEventListener('visibilitychange', () => { if (!document.hidden) loadTimesheetsTab(); });
}

function ptTickTimers() {
    const all     = CONFIG.data.timesheets.activeTimers || [];
    const visible = ptTimerExpanded ? all : all.slice(0, PT_TIMER_PAGE);
    visible.forEach(e => {
        const el = document.getElementById('pt-elapsed-' + e.timesheetId);
        if (!el) return;
        const started = ptStartTimes[e.userEmail] || Date.now();
        const diff = Math.floor((Date.now() - started) / 1000);
        const h = Math.floor(diff / 3600), m = Math.floor((diff % 3600) / 60), s = diff % 60;
        el.textContent = `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
    });
}

function togglePtTimers() {
    ptTimerExpanded = !ptTimerExpanded;
    ptRenderActiveTimers(CONFIG.data.timesheets.activeTimers);
}

async function ptStopTimer(timesheetId, userEmail) {
    try {
        const orgName = await IndexLibrary.GetAccountNameAsync();
        await IndexLibrary.PostRequest({ endpoint: '/api/Timesheet/StopTimeLogging', body: { organizationName: orgName, workItemId: '', userEmail } });
        await loadTimesheetsTab();
    } catch (e) {
        console.warn('[timesheets] StopTimeLogging failed:', e.message);
    }
}