"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDaysFilterNotice = buildDaysFilterNotice;
exports.buildApplicationFilterNotice = buildApplicationFilterNotice;
exports.applyArgusScopeNoticesToAnswer = applyArgusScopeNoticesToAnswer;
const drizzle_orm_1 = require("drizzle-orm");
const database_1 = require("../../../../database");
const tables_1 = require("../../../../database/tables");
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function normalizeUuid(id) {
    return id.trim().toLowerCase();
}
function extractDayIntervalFromSql(sql) {
    if (!sql || typeof sql !== 'string') {
        return undefined;
    }
    const intervalMatch = sql.match(/\bINTERVAL\s+(\d+)\s+DAY\b/i);
    if (intervalMatch) {
        const n = Number.parseInt(intervalMatch[1], 10);
        return Number.isFinite(n) && n > 0 ? n : undefined;
    }
    const toIntervalMatch = sql.match(/\btoIntervalDay\s*\(\s*(\d+)\s*\)/i);
    if (toIntervalMatch) {
        const n = Number.parseInt(toIntervalMatch[1], 10);
        return Number.isFinite(n) && n > 0 ? n : undefined;
    }
    return undefined;
}
function buildDaysFilterNotice(apiCalls, filters) {
    let daysFromSql;
    for (const call of apiCalls) {
        const sql = typeof call?.sql === 'string' ? call.sql : '';
        const d = extractDayIntervalFromSql(sql);
        if (d !== undefined) {
            daysFromSql = d;
            break;
        }
    }
    let daysFromFilters;
    if (filters && typeof filters === 'object') {
        const raw = filters.days;
        if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
            daysFromFilters = raw;
        }
        else if (typeof raw === 'string' && raw.trim() !== '') {
            const n = Number.parseInt(raw, 10);
            if (Number.isFinite(n) && n > 0) {
                daysFromFilters = n;
            }
        }
    }
    const days = daysFromSql ?? daysFromFilters;
    if (days !== undefined && days > 0) {
        return days === 1
            ? 'These results are for the last 1 day.'
            : `These results are for the last ${days} days.`;
    }
    const dr = filters && typeof filters === 'object'
        ? filters.dateRange
        : undefined;
    if (dr && typeof dr === 'object') {
        const start = dr.start ?? dr.from;
        const end = dr.end ?? dr.to;
        if (typeof start === 'string' && typeof end === 'string' && start && end) {
            return `These results are for the period from ${start} to ${end}.`;
        }
    }
    return undefined;
}
function extractUuidEquality(sql) {
    const m = sql.match(/\b(?:ServiceName|application_id)\s*=\s*'([0-9a-f-]{36})'/i);
    if (!m?.[1]) {
        return undefined;
    }
    return normalizeUuid(m[1]);
}
function extractUuidInClause(sql, column) {
    const re = new RegExp(`\\b${column}\\s+IN\\s*\\(([^)]*)\\)`, 'gi');
    const seen = new Set();
    let match = re.exec(sql);
    while (match !== null) {
        const inner = match[1];
        const parts = inner.split(',');
        for (const p of parts) {
            const u = p.trim().replace(/^['"]|['"]$/g, '');
            if (UUID_RE.test(u)) {
                seen.add(normalizeUuid(u));
            }
        }
        match = re.exec(sql);
    }
    return [...seen];
}
/** Human-readable fragment from JOIN applications ... app.name ILIKE '%x%' */
function extractApplicationNameIlikeFragment(sql) {
    const m = sql.match(/\b(?:app|applications)\.name\s+ILIKE\s+'%([^'%]*)%'/i);
    if (m?.[1] != null && String(m[1]).trim() !== '') {
        return String(m[1]).trim();
    }
    const m2 = sql.match(/\b(?:app|applications)\.name\s+ILIKE\s+'([^']*)'/i);
    if (m2?.[1]) {
        const raw = String(m2[1]).trim().replace(/^%+|%+$/g, '');
        if (raw) {
            return raw;
        }
    }
    return undefined;
}
function isFullProjectInList(ids, projectIds) {
    if (!projectIds || projectIds.size === 0 || ids.length === 0) {
        return false;
    }
    if (ids.length !== projectIds.size) {
        return false;
    }
    return ids.every((id) => projectIds.has(normalizeUuid(id)));
}
async function loadProjectApplicationIdSet(projectId) {
    try {
        const db = await (0, database_1.getDb)();
        const rows = await db
            .select({ id: tables_1.applicationsTable.id })
            .from(tables_1.applicationsTable)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId), (0, drizzle_orm_1.eq)(tables_1.applicationsTable.is_archived, false)));
        const set = new Set();
        for (const r of rows) {
            if (r.id) {
                set.add(normalizeUuid(String(r.id)));
            }
        }
        return set;
    }
    catch {
        return new Set();
    }
}
async function loadApplicationDisplayNames(projectId, ids) {
    const uniq = [...new Set(ids.map((id) => normalizeUuid(id)))].filter((id) => UUID_RE.test(id));
    if (uniq.length === 0) {
        return new Map();
    }
    try {
        const db = await (0, database_1.getDb)();
        const rows = await db
            .select({ id: tables_1.applicationsTable.id, name: tables_1.applicationsTable.name })
            .from(tables_1.applicationsTable)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, projectId), (0, drizzle_orm_1.inArray)(tables_1.applicationsTable.id, uniq)));
        const m = new Map();
        for (const r of rows) {
            if (r.id) {
                m.set(normalizeUuid(String(r.id)), String(r.name ?? '').trim() || String(r.id));
            }
        }
        return m;
    }
    catch {
        return new Map();
    }
}
function displayNameForId(id, idToName) {
    const k = normalizeUuid(id);
    return idToName.get(k) ?? idToName.get(id);
}
/**
 * One-line notice when results are scoped to a specific application or name pattern.
 * Skips the default project-wide `ServiceName/application_id IN (all apps)` injection used by Argus.
 */
function buildApplicationFilterNotice(apiCalls, filters, projectApplicationIds, idToName) {
    const sqlBlob = apiCalls.map((c) => (typeof c.sql === 'string' ? c.sql : '')).join('\n');
    const ilikeFrag = extractApplicationNameIlikeFragment(sqlBlob);
    if (ilikeFrag) {
        return `These results filter applications whose name matches "${ilikeFrag}".`;
    }
    const filterAppRaw = filters && typeof filters.applicationId === 'string' ? filters.applicationId.trim() : '';
    const filterAppId = filterAppRaw && UUID_RE.test(filterAppRaw) ? normalizeUuid(filterAppRaw) : '';
    const snIn = extractUuidInClause(sqlBlob, 'ServiceName');
    const apIn = extractUuidInClause(sqlBlob, 'application_id');
    const eqId = extractUuidEquality(sqlBlob);
    const serviceNameFull = snIn.length > 0 &&
        projectApplicationIds.size > 0 &&
        isFullProjectInList(snIn, projectApplicationIds);
    const appIdFull = apIn.length > 0 &&
        projectApplicationIds.size > 0 &&
        isFullProjectInList(apIn, projectApplicationIds);
    const onlyBulkProjectIn = !ilikeFrag &&
        !filterAppId &&
        !eqId &&
        (serviceNameFull || appIdFull);
    if (onlyBulkProjectIn) {
        return undefined;
    }
    if (filterAppId) {
        const label = displayNameForId(filterAppId, idToName);
        if (label) {
            return `These results are scoped to application "${label}".`;
        }
        return 'These results are scoped to the selected application (context).';
    }
    if (eqId) {
        const label = displayNameForId(eqId, idToName);
        if (label) {
            return `These results are scoped to application "${label}".`;
        }
        return 'These results are scoped to one application (from the query filter).';
    }
    const mergedIn = [...new Set([...snIn, ...apIn])];
    if (mergedIn.length === 1 && !serviceNameFull && !appIdFull) {
        const id = mergedIn[0];
        const label = displayNameForId(id, idToName);
        if (label) {
            return `These results are scoped to application "${label}".`;
        }
        return 'These results are scoped to one application (from the query filter).';
    }
    if (mergedIn.length > 1 && !serviceNameFull && !appIdFull) {
        return `These results are scoped to ${mergedIn.length} applications (from the query filter).`;
    }
    return undefined;
}
function collectIdsForApplicationLabels(apiCalls, filters) {
    const sqlBlob = apiCalls.map((c) => (typeof c.sql === 'string' ? c.sql : '')).join('\n');
    const ids = new Set();
    const filterRaw = filters && typeof filters.applicationId === 'string' ? filters.applicationId.trim() : '';
    if (filterRaw && UUID_RE.test(filterRaw)) {
        ids.add(normalizeUuid(filterRaw));
    }
    const eq = extractUuidEquality(sqlBlob);
    if (eq) {
        ids.add(eq);
    }
    const snIn = extractUuidInClause(sqlBlob, 'ServiceName');
    const apIn = extractUuidInClause(sqlBlob, 'application_id');
    const merged = [...new Set([...snIn, ...apIn])];
    if (merged.length === 1) {
        ids.add(merged[0]);
    }
    return [...ids];
}
/** Strip trailing punctuation / soften whitespace for "already said this" checks. */
function normalizeScopePhrase(s) {
    return s
        .trim()
        .toLowerCase()
        .replace(/\s+/g, ' ')
        .replace(/[.,;:]+$/, '');
}
/**
 * True if the answer already conveys the same scope line (model may use a comma instead of a period,
 * or merge time + app into one sentence).
 */
function lineAlreadyInAnswer(answer, line) {
    const a = normalizeScopePhrase(answer);
    const core = normalizeScopePhrase(line);
    if (!core) {
        return true;
    }
    if (a.includes(core)) {
        return true;
    }
    // Days notice: allow "last N days" without requiring exact "These results..." prefix
    const daysM = line.match(/last\s+(\d+)\s+days?/i);
    if (daysM) {
        const re = new RegExp(`these\\s+results[^.]{0,80}last\\s+${daysM[1]}\\s+days?`, 'i');
        if (re.test(a)) {
            return true;
        }
    }
    // App name ILIKE notice
    const nameM = line.match(/name matches "([^"]+)"/i);
    if (nameM) {
        const frag = normalizeScopePhrase(nameM[1]);
        if (frag && (a.includes(frag) || a.includes(`"${nameM[1].toLowerCase()}"`))) {
            return true;
        }
    }
    return false;
}
function shouldSkipScopeNotice(answer, scopeText) {
    const lines = scopeText
        .split('\n')
        .map((l) => l.trim())
        .filter(Boolean);
    if (lines.length === 0) {
        return true;
    }
    return lines.every((line) => lineAlreadyInAnswer(answer, line));
}
/**
 * Single-sentence footer when both time and application scope apply, so we do not repeat
 * "These results..." twice (reads better and dedupes against model paraphrases).
 */
function buildScopeFooterText(daysNotice, appNotice) {
    if (!daysNotice && !appNotice) {
        return '';
    }
    if (daysNotice && !appNotice) {
        return daysNotice;
    }
    if (!daysNotice && appNotice) {
        return appNotice;
    }
    const d = String(daysNotice).replace(/\.\s*$/, '');
    const app = String(appNotice);
    const ill = app.match(/name matches "([^"]+)"/i);
    const daysNum = d.match(/last\s+(\d+)\s+days?/i);
    if (ill && daysNum) {
        const n = Number.parseInt(daysNum[1], 10);
        const window = n === 1 ? 'the last 1 day' : `the last ${n} days`;
        return `These results are for ${window} for applications whose name matches "${ill[1]}".`;
    }
    const scoped = app.match(/scoped to application "([^"]+)"/i);
    if (scoped && daysNum) {
        const n = Number.parseInt(daysNum[1], 10);
        const window = n === 1 ? 'the last 1 day' : `the last ${n} days`;
        return `These results are for ${window}, scoped to application "${scoped[1]}".`;
    }
    if (app.includes('selected application (context)')) {
        return `${d}, scoped to the selected application (context).`;
    }
    if (app.includes('one application (from the query filter)')) {
        return `${d}, scoped to one application from the query filter.`;
    }
    const multi = app.match(/(\d+)\s+applications\s+\(from the query filter\)/i);
    if (multi) {
        return `${d}, scoped to ${multi[1]} applications from the query filter.`;
    }
    return `${d}.\n${app}`;
}
/**
 * Appends time-window + application-scope lines at the end of the answer when tools/filters narrowed the data.
 * Does not mention the automatic "all applications in project" SQL `IN (...)` injection.
 */
async function applyArgusScopeNoticesToAnswer(answer, apiCalls, filters, responseFormat, projectId) {
    const daysNotice = buildDaysFilterNotice(apiCalls, filters);
    let projectAppIds = new Set();
    const pid = typeof projectId === 'string' ? projectId.trim() : '';
    if (pid) {
        projectAppIds = await loadProjectApplicationIdSet(pid);
    }
    const idsForNames = collectIdsForApplicationLabels(apiCalls, filters);
    const idToName = pid && idsForNames.length > 0 ? await loadApplicationDisplayNames(pid, idsForNames) : new Map();
    const appNotice = buildApplicationFilterNotice(apiCalls, filters, projectAppIds, idToName);
    const scopeFooter = buildScopeFooterText(daysNotice, appNotice);
    if (!scopeFooter) {
        return answer;
    }
    const trimmed = answer.trim();
    if (shouldSkipScopeNotice(trimmed, scopeFooter)) {
        return answer;
    }
    if (responseFormat === 'graph') {
        if (!trimmed.startsWith('{')) {
            return answer;
        }
        try {
            const obj = JSON.parse(trimmed);
            const prev = typeof obj.explanation === 'string' && obj.explanation.trim()
                ? String(obj.explanation).trim()
                : '';
            obj.explanation = prev ? `${prev}\n\n${scopeFooter}` : scopeFooter;
            return JSON.stringify(obj);
        }
        catch {
            return answer;
        }
    }
    if (!trimmed) {
        return scopeFooter;
    }
    return `${trimmed}\n\n${scopeFooter}`;
}
//# sourceMappingURL=argusScopeNotices.js.map