"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEscalation = createEscalation;
exports.claimEscalation = claimEscalation;
exports.resolveEscalation = resolveEscalation;
exports.updateEscalationsPriority = updateEscalationsPriority;
exports.getEscalationRoles = getEscalationRoles;
exports.releaseEscalation = releaseEscalation;
exports.releaseExpiredClaims = releaseExpiredClaims;
exports.escalateToRole = escalateToRole;
exports.getEscalation = getEscalation;
exports.getEscalationsByTaskId = getEscalationsByTaskId;
exports.getEscalationsByWorkflowId = getEscalationsByWorkflowId;
exports.updateEscalationMetadata = updateEscalationMetadata;
exports.enrichEscalationRouting = enrichEscalationRouting;
exports.getEscalationsByOriginId = getEscalationsByOriginId;
exports.findByMetadata = findByMetadata;
exports.claimByMetadata = claimByMetadata;
exports.resolveByMetadataAtomic = resolveByMetadataAtomic;
const db_1 = require("../../lib/db");
const publish_1 = require("../../lib/events/publish");
const logger_1 = require("../../lib/logger");
const sql_1 = require("./sql");
async function createEscalation(input) {
    logger_1.loggerRegistry.info(`[escalation-crud] createEscalation called for wf=${input.workflow_id} type=${input.type} caller=${new Error().stack?.split('\n')[2]?.trim()}`);
    const pool = (0, db_1.getPool)();
    // Ensure the role exists in lt_roles (FK constraint)
    await pool.query(sql_1.ENSURE_ROLE_EXISTS, [input.role]);
    const { rows } = await pool.query(sql_1.CREATE_ESCALATION, [
        input.type,
        input.subtype,
        input.description || null,
        input.priority || 2,
        input.task_id || null,
        input.origin_id || null,
        input.parent_id || null,
        input.role,
        input.envelope,
        input.metadata ? JSON.stringify(input.metadata) : null,
        input.escalation_payload || null,
        input.workflow_id || null,
        input.task_queue || null,
        input.workflow_type || null,
        input.trace_id || null,
        input.span_id || null,
    ]);
    const escalation = rows[0];
    (0, publish_1.publishEscalationEvent)({
        type: 'escalation.created',
        source: 'service',
        workflowId: escalation.workflow_id || '',
        workflowName: escalation.workflow_type || '',
        taskQueue: escalation.task_queue || '',
        escalationId: escalation.id,
        status: 'pending',
        data: { type: input.type, role: input.role },
    });
    return escalation;
}
/**
 * Atomic claim operation. Does NOT change status — "claimed" is implicit
 * via assigned_to + assigned_until > NOW().
 *
 * Conditions:
 * - status = 'pending' (not resolved/cancelled)
 * - Either: unassigned, expired claim, or same user (extension)
 *
 * Uses a CTE to capture the previous state so callers can detect extensions.
 */
async function claimEscalation(id, userId, durationMinutes = 30) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.CLAIM_ESCALATION, [id, userId, durationMinutes]);
    if (rows.length === 0)
        return null;
    const row = rows[0];
    const escalation = row;
    (0, publish_1.publishEscalationEvent)({
        type: 'escalation.claimed',
        source: 'service',
        workflowId: escalation.workflow_id || '',
        workflowName: escalation.workflow_type || '',
        taskQueue: escalation.task_queue || '',
        escalationId: escalation.id,
        status: 'claimed',
        data: { assigned_to: userId },
    });
    return {
        escalation,
        isExtension: row.prev_assigned_to === userId,
    };
}
async function resolveEscalation(id, resolverPayload) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.RESOLVE_ESCALATION, [id, JSON.stringify(resolverPayload)]);
    const escalation = rows[0] || null;
    if (escalation) {
        (0, publish_1.publishEscalationEvent)({
            type: 'escalation.resolved',
            source: 'service',
            workflowId: escalation.workflow_id || '',
            workflowName: escalation.workflow_type || '',
            taskQueue: escalation.task_queue || '',
            escalationId: escalation.id,
            status: 'resolved',
            data: {},
        });
    }
    return escalation;
}
/**
 * Bulk update priority for a set of escalations.
 * Only updates pending escalations.
 */
async function updateEscalationsPriority(ids, priority) {
    if (ids.length === 0)
        return 0;
    const pool = (0, db_1.getPool)();
    const { rowCount } = await pool.query(sql_1.UPDATE_ESCALATIONS_PRIORITY, [priority, ids]);
    return rowCount ?? 0;
}
/**
 * Get the distinct roles for a set of escalation IDs.
 * Used for permission validation before bulk operations.
 */
async function getEscalationRoles(ids) {
    if (ids.length === 0)
        return [];
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.GET_ESCALATION_ROLES, [ids]);
    return rows.map((r) => r.role);
}
/**
 * Release a single escalation claim back to the available pool.
 * Only the assigned user (or superadmin via route) may release.
 */
async function releaseEscalation(id, userId) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.RELEASE_ESCALATION, [id, userId]);
    const released = rows[0];
    if (released) {
        (0, publish_1.publishEscalationEvent)({
            type: 'escalation.released',
            source: 'service',
            workflowId: released.workflow_id || '',
            workflowName: released.workflow_type || '',
            taskQueue: released.task_queue || '',
            escalationId: released.id,
            status: 'released',
            data: { released_by: userId },
        });
    }
    return released || null;
}
async function releaseExpiredClaims() {
    const pool = (0, db_1.getPool)();
    const { rowCount } = await pool.query(sql_1.RELEASE_EXPIRED_CLAIMS);
    return rowCount || 0;
}
/**
 * Reassign an escalation to a different role.
 * Clears the current assignment so it becomes available to the new role.
 */
async function escalateToRole(id, targetRole) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.ESCALATE_TO_ROLE, [id, targetRole]);
    return rows[0] || null;
}
async function getEscalation(id) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.GET_ESCALATION, [id]);
    return rows[0] || null;
}
async function getEscalationsByTaskId(taskId) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_TASK_ID, [taskId]);
    return rows;
}
async function getEscalationsByWorkflowId(workflowId) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_WORKFLOW_ID, [workflowId]);
    return rows;
}
async function updateEscalationMetadata(id, patch) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.UPDATE_ESCALATION_METADATA, [id, JSON.stringify(patch)]);
    return rows[0] || null;
}
async function enrichEscalationRouting(id, metadataPatch, workflowFields) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.ENRICH_ESCALATION_ROUTING, [
        id,
        JSON.stringify(metadataPatch),
        workflowFields.workflowType || null,
        workflowFields.workflowId || null,
        workflowFields.taskQueue || null,
        workflowFields.taskId || null,
    ]);
    return rows[0] || null;
}
async function getEscalationsByOriginId(originId) {
    const pool = (0, db_1.getPool)();
    const { rows } = await pool.query(sql_1.GET_ESCALATIONS_BY_ORIGIN_ID, [originId]);
    return rows;
}
// --- Metadata candidate key lookups -----------------------------------------
async function findByMetadata(key, value, status, limit = 50, offset = 0) {
    const pool = (0, db_1.getPool)();
    const filter = JSON.stringify({ [key]: value });
    const { rows } = await pool.query(sql_1.FIND_BY_METADATA, [filter, status || null, limit, offset]);
    const total = rows.length > 0 ? parseInt(rows[0]._total, 10) : 0;
    // Strip the window function column from results
    const escalations = rows.map(({ _total, ...rest }) => rest);
    return { escalations, total };
}
/**
 * Atomic claim by metadata with inline RBAC.
 * The SQL WHERE clause enforces role membership — if the caller
 * doesn't have an allowed role, zero rows match and the claim
 * never happens. No pre-flight find, no TOCTOU.
 *
 * @param allowedRoles — roles the caller can claim (null = no filter / global access)
 * @returns `{ escalation, isExtension, candidatesExist }` or null
 */
async function claimByMetadata(key, value, userId, durationMinutes = 30, metadata, allowedRoles) {
    const pool = (0, db_1.getPool)();
    const filter = JSON.stringify({ [key]: value });
    const metaPatch = metadata ? JSON.stringify(metadata) : null;
    const roles = allowedRoles ?? null;
    const { rows } = await pool.query(sql_1.CLAIM_BY_METADATA_GUARDED, [filter, userId, durationMinutes, metaPatch, roles]);
    if (rows.length === 0)
        return null;
    const row = rows[0];
    const { candidates_exist, prev_assigned_to, _total, ...rest } = row;
    const escalation = rest;
    (0, publish_1.publishEscalationEvent)({
        type: 'escalation.claimed',
        source: 'service',
        workflowId: escalation.workflow_id || '',
        workflowName: escalation.workflow_type || '',
        taskQueue: escalation.task_queue || '',
        escalationId: escalation.id,
        status: 'claimed',
        data: { assigned_to: userId },
    });
    return {
        escalation,
        isExtension: prev_assigned_to === userId,
        candidatesExist: parseInt(candidates_exist, 10),
    };
}
/**
 * Atomic resolve by metadata with signal guard.
 *
 * Single query, two outcomes:
 * 1. No signal_id → claim + resolve atomically. Returns { outcome: 'resolved', escalation }.
 * 2. signal_id present → resolve skipped. Returns { outcome: 'signal_required', signalId, escalationId, ... }
 *    so the caller can signal the workflow. conditionLT handles the rest.
 */
async function resolveByMetadataAtomic(key, value, userId, resolverPayload, metadata, allowedRoles) {
    const pool = (0, db_1.getPool)();
    const filter = JSON.stringify({ [key]: value });
    const payloadJson = JSON.stringify(resolverPayload);
    const metaPatch = metadata ? JSON.stringify(metadata) : null;
    const roles = allowedRoles ?? null;
    const { rows } = await pool.query(sql_1.RESOLVE_BY_METADATA_ATOMIC, [filter, userId, payloadJson, metaPatch, roles]);
    if (rows.length === 0)
        return { outcome: 'not_found' };
    const row = rows[0];
    if (row.outcome === 'resolved') {
        const { target_id, signal_id, target_workflow_id, target_workflow_type, target_task_queue, outcome, ...rest } = row;
        const escalation = rest;
        (0, publish_1.publishEscalationEvent)({
            type: 'escalation.resolved',
            source: 'service',
            workflowId: escalation.workflow_id || '',
            workflowName: escalation.workflow_type || '',
            taskQueue: escalation.task_queue || '',
            escalationId: escalation.id,
            status: 'resolved',
            data: { resolved_by: userId },
        });
        return { outcome: 'resolved', escalation };
    }
    // Signal-backed escalation — return the signal info for the caller
    return {
        outcome: 'signal_required',
        signalId: row.signal_id,
        escalationId: row.target_id,
        workflowId: row.target_workflow_id,
        workflowType: row.target_workflow_type,
        taskQueue: row.target_task_queue,
    };
}
