import { createLogger } from "../../utils/logger.js";
import { sendProgress } from "../../utils/progress.js";
import { isRecord, asRecord, asString, asNumber } from "../../utils/type-guards.js";
import { resolveLogContent } from "../../utils/log-resolver.js";
const log = createLogger("diagnose:pipeline");
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatDuration(ms) {
    if (ms < 1000)
        return `${ms}ms`;
    const seconds = Math.floor(ms / 1000);
    if (seconds < 60)
        return `${seconds}s`;
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    if (minutes < 60)
        return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
    const hours = Math.floor(minutes / 60);
    const remainingMinutes = minutes % 60;
    return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
function collectSteps(layoutNodeMap, nodeId, steps, visited, nodeMap) {
    if (visited.has(nodeId))
        return;
    visited.add(nodeId);
    const node = layoutNodeMap[nodeId];
    if (!node)
        return;
    const startTs = node.startTs;
    const endTs = node.endTs;
    const durationMs = startTs && endTs ? endTs - startTs : undefined;
    const failureMsg = node.failureInfo?.message ||
        nodeMap?.[nodeId]?.failureInfo?.message;
    steps.push({
        name: node.name ?? nodeId,
        identifier: node.nodeIdentifier ?? nodeId,
        status: node.status ?? "Unknown",
        duration_ms: durationMs,
        duration_human: durationMs != null ? formatDuration(durationMs) : undefined,
        failure_message: failureMsg || undefined,
    });
    for (const childId of node.edgeLayoutList?.currentNodeChildren ?? []) {
        collectSteps(layoutNodeMap, childId, steps, visited, nodeMap);
    }
    for (const nextId of node.edgeLayoutList?.nextIds ?? []) {
        collectSteps(layoutNodeMap, nextId, steps, visited, nodeMap);
    }
}
function extractStages(layoutNodeMap, startingNodeId, nodeMap) {
    const stages = [];
    const visited = new Set();
    function walkNode(nodeId) {
        if (visited.has(nodeId))
            return;
        visited.add(nodeId);
        const node = layoutNodeMap[nodeId];
        if (!node)
            return;
        if (node.nodeGroup === "STAGE") {
            const startTs = node.startTs;
            const endTs = node.endTs;
            const durationMs = startTs && endTs ? endTs - startTs : undefined;
            const steps = [];
            const stepVisited = new Set();
            for (const childId of node.edgeLayoutList?.currentNodeChildren ?? []) {
                collectSteps(layoutNodeMap, childId, steps, stepVisited, nodeMap);
            }
            stages.push({
                name: node.name ?? nodeId,
                identifier: node.nodeIdentifier ?? nodeId,
                status: node.status ?? "Unknown",
                started_at: startTs ? new Date(startTs).toISOString() : undefined,
                ended_at: endTs ? new Date(endTs).toISOString() : undefined,
                duration_ms: durationMs,
                duration_human: durationMs != null ? formatDuration(durationMs) : undefined,
                failure_message: node.failureInfo?.message ||
                    nodeMap?.[nodeId]?.failureInfo?.message,
                steps,
            });
        }
        else {
            for (const childId of node.edgeLayoutList?.currentNodeChildren ?? []) {
                walkNode(childId);
            }
        }
        for (const nextId of node.edgeLayoutList?.nextIds ?? []) {
            walkNode(nextId);
        }
    }
    walkNode(startingNodeId);
    return stages;
}
function findFailedNodes(nodeMap) {
    const stepNodes = [];
    const stageNodes = [];
    for (const node of Object.values(nodeMap)) {
        if (node.status !== "Failed" && node.status !== "Errored" && node.status !== "Aborted")
            continue;
        const msg = node.failureInfo?.message;
        if (!msg)
            continue;
        const fqn = node.baseFqn ?? "";
        if (node.identifier === "pipeline" || fqn === "pipeline")
            continue;
        const stageMatch = fqn.match(/\.stages\.([^.]+)\./);
        const stageId = stageMatch?.[1] ?? node.identifier ?? "unknown";
        if (!stageMatch && fqn.startsWith("pipeline.stages") && !fqn.includes(".spec."))
            continue;
        const delegate = node.delegateInfoList?.[0]?.name;
        const detail = {
            stage: stageId,
            step: node.identifier ?? node.name ?? "unknown",
            failure_message: msg,
            log_key: node.logBaseKey,
            delegate,
        };
        if (node.stepType === "ShellScript" && node.stepParameters?.spec) {
            const spec = node.stepParameters.spec;
            const scriptSource = spec.source?.spec?.script;
            const envVars = spec.environmentVariables;
            const hasRetry = node.interruptHistories?.some((h) => h.interruptType === "RETRY");
            if (scriptSource || envVars) {
                detail.script_context = {
                    name: node.stepParameters.name ?? node.name,
                    timeout: node.stepParameters.timeout,
                    shell: spec.shell,
                    script: scriptSource,
                    env_vars: envVars && Object.keys(envVars).length > 0 ? envVars : undefined,
                    retried: hasRetry || undefined,
                };
            }
        }
        if (fqn.includes(".steps.")) {
            stepNodes.push(detail);
        }
        else {
            stageNodes.push(detail);
        }
    }
    return stepNodes.length > 0 ? stepNodes : stageNodes;
}
function findChildPipelineRef(nodeMap) {
    for (const node of Object.values(nodeMap)) {
        if (node.status !== "Failed" && node.status !== "Errored" && node.status !== "Aborted")
            continue;
        const child = node.stepDetails?.childPipelineExecutionDetails;
        if (child?.planExecutionId) {
            return { executionId: child.planExecutionId, orgId: child.orgId ?? "", projectId: child.projectId ?? "" };
        }
    }
    return undefined;
}
async function diagnoseChildPipeline(client, child, signal) {
    try {
        const response = await client.request({
            method: "GET",
            path: `/pipeline/api/pipelines/execution/v2/${child.executionId}`,
            params: {
                orgIdentifier: child.orgId,
                projectIdentifier: child.projectId,
                renderFullBottomGraph: "true",
            },
            signal,
        });
        const responseRec = asRecord(response) ?? {};
        const data = asRecord(responseRec.data) ?? responseRec;
        const execGraph = asRecord(data.executionGraph);
        const graphNodeMap = asRecord(execGraph?.nodeMap);
        if (graphNodeMap)
            return findFailedNodes(graphNodeMap);
    }
    catch (err) {
        log.warn("Child pipeline diagnosis failed", { executionId: child.executionId, error: String(err) });
    }
    return [];
}
function buildExecutionSummary(execution, config, input, accountId) {
    const pes = asRecord(execution.pipelineExecutionSummary);
    if (!pes)
        return { summary: execution, failedNodes: [] };
    const startTs = asNumber(pes.startTs);
    const endTs = asNumber(pes.endTs);
    const durationMs = startTs && endTs ? endTs - startTs : undefined;
    const triggerInfo = asRecord(pes.executionTriggerInfo);
    const triggeredBy = asRecord(triggerInfo?.triggeredBy);
    const summary = {
        pipeline: {
            name: pes.name,
            identifier: pes.pipelineIdentifier,
        },
        execution: {
            id: pes.planExecutionId,
            status: pes.status,
            run_sequence: pes.runSequence,
            trigger_type: triggerInfo?.triggerType,
            triggered_by: triggeredBy?.identifier ?? asRecord(triggeredBy?.extraInfo)?.email,
        },
        timing: {
            started_at: startTs ? new Date(startTs).toISOString() : undefined,
            ended_at: endTs ? new Date(endTs).toISOString() : undefined,
            duration_ms: durationMs,
            duration_human: durationMs != null ? formatDuration(durationMs) : undefined,
        },
    };
    const layoutNodeMap = isRecord(pes.layoutNodeMap) ? pes.layoutNodeMap : undefined;
    const startingNodeId = asString(pes.startingNodeId);
    const executionGraph = asRecord(execution.executionGraph);
    const nodeMap = isRecord(executionGraph?.nodeMap) ? executionGraph.nodeMap : undefined;
    if (layoutNodeMap && startingNodeId) {
        const stages = extractStages(layoutNodeMap, startingNodeId, nodeMap);
        summary.stages = stages;
        const completedStages = stages.filter((s) => s.duration_ms != null && s.duration_ms > 0);
        if (completedStages.length > 0 && durationMs && durationMs > 0) {
            const bottleneck = completedStages.reduce((a, b) => (a.duration_ms > b.duration_ms ? a : b));
            summary.bottleneck = {
                stage: bottleneck.name,
                duration_ms: bottleneck.duration_ms,
                duration_human: bottleneck.duration_human,
                percentage: Math.round((bottleneck.duration_ms / durationMs) * 100),
            };
        }
    }
    let failedNodes = [];
    let childRef;
    if (nodeMap) {
        failedNodes = findFailedNodes(nodeMap);
        childRef = findChildPipelineRef(nodeMap);
    }
    if (failedNodes.length > 0) {
        const primary = failedNodes[0];
        const failureEntry = (f) => {
            const entry = {
                stage: f.stage,
                step: f.step,
                error: f.failure_message,
                delegate: f.delegate,
            };
            if (f.script_context)
                entry.script_context = f.script_context;
            return entry;
        };
        summary.failure = failureEntry(primary);
        if (failedNodes.length > 1) {
            summary.all_failures = failedNodes.map(failureEntry);
        }
    }
    else {
        const stages = summary.stages;
        const failedStage = stages?.find((s) => s.status === "Failed" || s.status === "Errored" || s.status === "Aborted");
        if (failedStage) {
            const failedStep = failedStage.steps.find((s) => s.failure_message);
            summary.failure = {
                stage: failedStage.name,
                step: failedStep?.name,
                error: failedStep?.failure_message ?? failedStage.failure_message,
            };
        }
    }
    const orgId = asString(input.org_id) ?? config.HARNESS_ORG;
    const projectId = asString(input.project_id) ?? config.HARNESS_PROJECT;
    const pipelineIdentifier = asString(pes.pipelineIdentifier);
    const execId = asString(pes.planExecutionId);
    if (pipelineIdentifier && execId && orgId && projectId) {
        const base = config.HARNESS_BASE_URL.replace(/\/$/, "");
        summary.openInHarness = `${base}/ng/account/${accountId}/all/orgs/${orgId}/projects/${projectId}/pipelines/${pipelineIdentifier}/deployments/${execId}/pipeline`;
    }
    else if (execution.openInHarness) {
        summary.openInHarness = execution.openInHarness;
    }
    return { summary, failedNodes, childRef };
}
function tailLines(text, n) {
    if (n <= 0)
        return { text, truncated: false, totalLines: text.split("\n").length };
    const lines = text.split("\n");
    if (lines.length <= n)
        return { text, truncated: false, totalLines: lines.length };
    return {
        text: `... (${lines.length - n} lines omitted) ...\n` + lines.slice(-n).join("\n"),
        truncated: true,
        totalLines: lines.length,
    };
}
function truncateLog(raw, maxLines) {
    if (typeof raw === "string") {
        const result = tailLines(raw, maxLines);
        if (!result.truncated)
            return raw;
        return { log_snippet: result.text, total_lines: result.totalLines, truncated: true };
    }
    return raw;
}
// ─── Handler ─────────────────────────────────────────────────────────────────
export const pipelineHandler = {
    entityType: "pipeline",
    description: "Analyze a pipeline execution — stage/step breakdown, timing, bottlenecks, failure details with exact step, error, delegate, and script context.",
    async diagnose(ctx) {
        const { client, registry, config, input, args, extra, signal } = ctx;
        let executionId = asString(input.execution_id);
        const pipelineId = asString(input.pipeline_id);
        const isSummary = args.summary !== false;
        const includeYaml = args.include_yaml ?? !isSummary;
        const includeLogs = args.include_logs ?? !isSummary;
        const logSnippetLines = asNumber(args.log_snippet_lines) ?? 120;
        const maxFailedSteps = asNumber(args.max_failed_steps) ?? 5;
        const hasRequestedStep = !!asString(input.step_id);
        let totalSteps = 1;
        if (includeYaml)
            totalSteps++;
        if (includeLogs)
            totalSteps++;
        if (includeLogs && hasRequestedStep)
            totalSteps++;
        if (!executionId && pipelineId) {
            log.info("Fetching latest execution for pipeline", { pipelineId });
            await sendProgress(extra, 0, totalSteps, "Fetching latest execution...");
            try {
                const execList = await registry.dispatch(client, "execution", "list", {
                    ...input,
                    pipeline_id: pipelineId,
                    size: 1,
                    page: 0,
                }, signal);
                const items = execList.items;
                if (items && items.length > 0) {
                    executionId = items[0].planExecutionId ?? undefined;
                    input.execution_id = executionId;
                }
            }
            catch (err) {
                log.warn("Failed to fetch latest execution", { error: String(err) });
            }
        }
        if (!executionId) {
            throw new Error("execution_id or pipeline_id is required. Provide either explicitly or via a Harness URL.");
        }
        const diagnostic = {};
        let currentStep = 0;
        let resolvedPipelineId;
        let failedNodes = [];
        let graphNodeMap;
        // step_id is extracted from the Harness URL's ?step= query param
        const requestedStepId = asString(input.step_id);
        await sendProgress(extra, currentStep, totalSteps, "Fetching execution details...");
        log.info("Fetching execution details", { executionId });
        try {
            const execution = await registry.dispatch(client, "execution", "get", {
                ...input,
                render_full_graph: true,
            }, signal);
            const exec = asRecord(execution) ?? {};
            const pes = asRecord(exec.pipelineExecutionSummary);
            resolvedPipelineId = asString(pes?.pipelineIdentifier);
            // Always extract the full graph node map — needed for both failed-step
            // log fetching and the explicit step log lookup below.
            const execGraph = asRecord(exec.executionGraph);
            graphNodeMap = isRecord(execGraph?.nodeMap) ? execGraph.nodeMap : undefined;
            if (isSummary) {
                const result = buildExecutionSummary(exec, config, input, registry.getAccountId());
                diagnostic.execution = result.summary;
                failedNodes = result.failedNodes;
                if (result.childRef) {
                    log.info("Detected chained pipeline failure, diagnosing child", result.childRef);
                    const childFailedNodes = await diagnoseChildPipeline(client, result.childRef, signal);
                    if (childFailedNodes.length > 0) {
                        const childEntry = (f) => {
                            const e = {
                                stage: f.stage, step: f.step, error: f.failure_message, delegate: f.delegate,
                            };
                            if (f.script_context)
                                e.script_context = f.script_context;
                            return e;
                        };
                        const childPrimary = childFailedNodes[0];
                        const execDiag = asRecord(diagnostic.execution) ?? {};
                        execDiag.child_pipeline = {
                            execution_id: result.childRef.executionId,
                            org_id: result.childRef.orgId,
                            project_id: result.childRef.projectId,
                            failure: childEntry(childPrimary),
                            all_failures: childFailedNodes.length > 1
                                ? childFailedNodes.map(childEntry)
                                : undefined,
                        };
                        failedNodes = childFailedNodes;
                    }
                }
            }
            else {
                diagnostic.execution = execution;
                if (graphNodeMap) {
                    failedNodes = findFailedNodes(graphNodeMap);
                }
            }
            currentStep++;
            if (includeYaml && resolvedPipelineId) {
                await sendProgress(extra, currentStep, totalSteps, "Fetching pipeline YAML...");
                try {
                    const pipeline = await registry.dispatch(client, "pipeline", "get", {
                        ...input,
                        pipeline_id: resolvedPipelineId,
                    }, signal);
                    diagnostic.pipeline = pipeline;
                }
                catch (err) {
                    log.warn("Failed to fetch pipeline YAML", { error: String(err) });
                    diagnostic.pipeline_error = String(err);
                }
                currentStep++;
            }
        }
        catch (err) {
            diagnostic.execution_error = String(err);
        }
        // Track which log keys were actually fetched for failed steps — used below to
        // avoid double-fetching the same log if step_id points to a failed step.
        // Must use `capped` (the actually-fetched subset), not the full `failedNodes`,
        // so that truncated failures don't incorrectly block the requested_step_log fetch.
        let fetchedFailedLogKeys = new Set();
        if (includeLogs && failedNodes.length > 0) {
            await sendProgress(extra, currentStep, totalSteps, "Fetching failed step logs...");
            const capped = maxFailedSteps > 0 ? failedNodes.slice(0, maxFailedSteps) : failedNodes;
            if (capped.length < failedNodes.length) {
                diagnostic.failed_steps_truncated = { shown: capped.length, total: failedNodes.length };
            }
            const logEntries = await Promise.all(capped.map(async (fn) => {
                const key = `${fn.stage}/${fn.step}`;
                const prefix = fn.log_key;
                if (!prefix)
                    return { key, value: { error: "No log key available for this step" } };
                fetchedFailedLogKeys.add(prefix);
                try {
                    const logText = await resolveLogContent(client, prefix, { signal });
                    return { key, value: truncateLog(logText, logSnippetLines) };
                }
                catch (err) {
                    log.warn("Failed to fetch step logs", { step: fn.step, error: String(err) });
                    return { key, value: { error: String(err) } };
                }
            }));
            const stepLogs = {};
            for (const entry of logEntries) {
                stepLogs[entry.key] = entry.value;
            }
            diagnostic.failed_step_logs = stepLogs;
        }
        // If a specific step was requested via the Harness URL (?step=<nodeExecutionId>),
        // fetch its log regardless of pass/fail status. The nodeMap key IS the nodeExecutionId.
        // Skip only if the step's log was actually fetched in the capped failed_step_logs above.
        const requestedLogKey = requestedStepId ? graphNodeMap?.[requestedStepId]?.logBaseKey : undefined;
        const alreadyFetchedAsFailedStep = !!requestedLogKey && fetchedFailedLogKeys.has(requestedLogKey);
        if (includeLogs && requestedStepId && graphNodeMap && !alreadyFetchedAsFailedStep) {
            await sendProgress(extra, currentStep, totalSteps, "Fetching requested step log...");
            const requestedNode = graphNodeMap[requestedStepId];
            if (requestedNode?.logBaseKey) {
                log.info("Fetching log for explicitly requested step", {
                    step_id: requestedStepId,
                    step: requestedNode.identifier ?? requestedNode.name,
                    status: requestedNode.status,
                });
                try {
                    const logText = await resolveLogContent(client, requestedNode.logBaseKey, { signal });
                    diagnostic.requested_step_log = {
                        step_id: requestedStepId,
                        step: requestedNode.identifier ?? requestedNode.name,
                        status: requestedNode.status,
                        log: truncateLog(logText, logSnippetLines),
                    };
                }
                catch (err) {
                    log.warn("Failed to fetch requested step log", { step_id: requestedStepId, error: String(err) });
                    diagnostic.requested_step_log = {
                        step_id: requestedStepId,
                        step: requestedNode.identifier ?? requestedNode.name,
                        status: requestedNode.status,
                        error: String(err),
                    };
                }
            }
            else if (requestedNode) {
                diagnostic.requested_step_log = {
                    step_id: requestedStepId,
                    step: requestedNode.identifier ?? requestedNode.name,
                    status: requestedNode.status,
                    error: "No logBaseKey available for this step — it may be part of a remote template whose graph is not exposed.",
                };
            }
            else {
                diagnostic.requested_step_log = {
                    step_id: requestedStepId,
                    error: "Step not found in execution graph. It may belong to a nested template pipeline.",
                };
            }
            currentStep++;
        }
        // Auto-fetch logs for successful executions when include_logs is true but
        // no failed steps and no explicit step was requested.  Pick the deepest
        // (most specific) Run/Script step from the graph — this is typically the
        // main build/test step whose output the user actually wants to read.
        if (includeLogs &&
            failedNodes.length === 0 &&
            !requestedStepId &&
            graphNodeMap &&
            !diagnostic.failed_step_logs &&
            !diagnostic.requested_step_log) {
            await sendProgress(extra, currentStep, totalSteps, "Fetching execution step logs...");
            // Find Run/Script steps with logBaseKeys, prefer the deepest (longest key)
            let bestNode;
            let bestNodeId;
            for (const [nodeId, node] of Object.entries(graphNodeMap)) {
                if (!node.logBaseKey)
                    continue;
                if (!bestNode || (node.logBaseKey.length > (bestNode.logBaseKey?.length ?? 0))) {
                    bestNode = node;
                    bestNodeId = nodeId;
                }
            }
            if (bestNode?.logBaseKey) {
                log.info("Auto-fetching log for main execution step", {
                    step_id: bestNodeId,
                    step: bestNode.identifier ?? bestNode.name,
                    status: bestNode.status,
                });
                try {
                    const logText = await resolveLogContent(client, bestNode.logBaseKey, { signal });
                    diagnostic.requested_step_log = {
                        step_id: bestNodeId,
                        step: bestNode.identifier ?? bestNode.name,
                        status: bestNode.status,
                        log: truncateLog(logText, logSnippetLines),
                    };
                }
                catch (err) {
                    log.warn("Failed to auto-fetch step log", { step_id: bestNodeId, error: String(err) });
                    diagnostic.requested_step_log = {
                        step_id: bestNodeId,
                        step: bestNode.identifier ?? bestNode.name,
                        status: bestNode.status,
                        error: String(err),
                    };
                }
            }
            currentStep++;
        }
        await sendProgress(extra, totalSteps, totalSteps, isSummary ? "Report complete" : "Diagnosis complete");
        return diagnostic;
    },
};
//# sourceMappingURL=pipeline.js.map