import { createLogger } from '@aztec/aztec.js/log';
import { promiseWithResolvers } from '@aztec/foundation/promise';
import { retryUntil } from '@aztec/foundation/retry';
import { exec, spawn } from 'child_process';
import path from 'path';
import { promisify } from 'util';
import { AlertTriggeredError, GrafanaClient } from '../../quality_of_service/grafana_client.js';
const execAsync = promisify(exec);
const logger = createLogger('e2e:k8s-utils');
export async function startPortForward({ resource, namespace, containerPort, hostPort }) {
    const hostPortAsString = hostPort ? hostPort.toString() : '';
    logger.debug(`kubectl port-forward -n ${namespace} ${resource} ${hostPortAsString}:${containerPort}`);
    const process = spawn('kubectl', [
        'port-forward',
        '-n',
        namespace,
        resource,
        `${hostPortAsString}:${containerPort}`
    ], {
        detached: true,
        windowsHide: true,
        stdio: [
            'ignore',
            'pipe',
            'pipe'
        ]
    });
    let isResolved = false;
    const connected = new Promise((resolve, reject)=>{
        process.stdout?.on('data', (data)=>{
            const str = data.toString();
            if (!isResolved && str.includes('Forwarding from')) {
                isResolved = true;
                logger.debug(`Port forward for ${resource}: ${str}`);
                const port = str.search(/:\d+/);
                if (port === -1) {
                    reject(new Error('Port not found in port forward output'));
                    return;
                }
                const portNumber = parseInt(str.slice(port + 1));
                logger.verbose(`Port forwarded for ${resource} at ${portNumber}:${containerPort}`);
                resolve(portNumber);
            } else {
                logger.silent(str);
            }
        });
        process.stderr?.on('data', (data)=>{
            logger.verbose(`Port forward for ${resource}: ${data.toString()}`);
            // It's a strange thing:
            // If we don't pipe stderr, then the port forwarding does not work.
            // Log to silent because this doesn't actually report errors,
            // just extremely verbose debug logs.
            logger.silent(data.toString());
        });
        process.on('close', ()=>{
            if (!isResolved) {
                isResolved = true;
                const msg = `Port forward for ${resource} closed before connection established`;
                logger.warn(msg);
                reject(new Error(msg));
            }
        });
        process.on('error', (error)=>{
            if (!isResolved) {
                isResolved = true;
                const msg = `Port forward for ${resource} error: ${error}`;
                logger.error(msg);
                reject(new Error(msg));
            }
        });
        process.on('exit', (code)=>{
            if (!isResolved) {
                isResolved = true;
                const msg = `Port forward for ${resource} exited with code ${code}`;
                logger.verbose(msg);
                reject(new Error(msg));
            }
        });
    });
    const port = await connected;
    return {
        process,
        port
    };
}
export function getExternalIP(namespace, serviceName) {
    const { promise, resolve, reject } = promiseWithResolvers();
    const process = spawn('kubectl', [
        'get',
        'service',
        '-n',
        namespace,
        `${namespace}-${serviceName}`,
        '--output',
        "jsonpath='{.status.loadBalancer.ingress[0].ip}'"
    ], {
        stdio: 'pipe'
    });
    let ip = '';
    process.stdout.on('data', (data)=>{
        ip += data;
    });
    process.on('error', (err)=>{
        reject(err);
    });
    process.on('exit', ()=>{
        // kubectl prints JSON. Remove the quotes
        resolve(ip.replace(/"|'/g, ''));
    });
    return promise;
}
/**
 * Gets an endpoint for a K8s service.
 * By default, tries to get the external IP first and falls back to port-forward if unavailable.
 *
 * @param opts.namespace - K8s namespace
 * @param opts.serviceName - Service name suffix (e.g., 'rpc-aztec-node', 'eth-execution')
 * @param opts.containerPort - Port the service exposes
 * @param opts.usePortForward - If true, skip external IP check and always use port-forward
 */ export async function getServiceEndpoint(opts) {
    const { namespace, serviceName, containerPort, forcePortForward } = opts;
    if (!forcePortForward) {
        try {
            const ip = await retryUntil(async ()=>{
                try {
                    const ip = await getExternalIP(namespace, serviceName);
                    if (ip && ip !== '' && ip !== '<pending>' && ip !== 'null') {
                        return ip;
                    }
                } catch (err) {
                    logger.verbose(`Failed to get external IP for ${serviceName}: ${err}`);
                }
                return undefined;
            }, `external IP for ${serviceName}`, 30, 5);
            logger.info(`Using external IP for ${serviceName}: ${ip}:${containerPort}`);
            return {
                url: `http://${ip}:${containerPort}`
            };
        } catch  {
            logger.warn(`External IP not available for ${serviceName} after 5min, using port-forward`);
        }
    }
    // Fallback to port-forward
    const resource = `svc/${namespace}-${serviceName}`;
    const { process, port } = await startPortForward({
        resource,
        namespace,
        containerPort
    });
    return {
        url: `http://127.0.0.1:${port}`,
        process
    };
}
/**
 * Gets an endpoint for the RPC node service.
 * Tries external IP first, falls back to port-forward.
 *
 * @param namespace - K8s namespace
 * @param usePortForward - If true, skip external IP and use port-forward directly
 */ export async function getRPCEndpoint(namespace, forcePortForward) {
    return await getServiceEndpoint({
        namespace,
        serviceName: 'rpc-aztec-node',
        containerPort: 8080,
        forcePortForward
    });
}
/**
 * Gets an endpoint for the Ethereum execution service.
 * Tries external IP first, falls back to port-forward.
 *
 * @param namespace - K8s namespace
 * @param usePortForward - If true, skip external IP and use port-forward directly
 */ export async function getEthereumEndpoint(namespace, forcePortForward) {
    return await getServiceEndpoint({
        namespace,
        serviceName: 'eth-execution',
        containerPort: 8545,
        forcePortForward
    });
}
export function startPortForwardForPrometeheus(namespace) {
    return startPortForward({
        resource: `svc/${namespace}-prometheus-server`,
        namespace,
        containerPort: 80
    });
}
export function startPortForwardForRPC(namespace, index = 0) {
    return startPortForward({
        resource: `pod/${namespace}-rpc-aztec-node-${index}`,
        namespace,
        containerPort: 8080
    });
}
export function startPortForwardForEthereum(namespace) {
    return startPortForward({
        resource: `services/${namespace}-eth-execution`,
        namespace,
        containerPort: 8545
    });
}
export async function deleteResourceByName({ resource, namespace, name, force = false }) {
    const command = `kubectl delete ${resource} ${name} -n ${namespace} --ignore-not-found=true --wait=true ${force ? '--force' : ''}`;
    logger.info(`command: ${command}`);
    const { stdout } = await execAsync(command);
    return stdout;
}
export async function deleteResourceByLabel({ resource, namespace, label, timeout = '5m', force = false }) {
    try {
        // Match both plain and group-qualified names (e.g., "podchaos" or "podchaos.chaos-mesh.org")
        const escaped = resource.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
        const regex = `(^|\\.)${escaped}(\\.|$)`;
        await execAsync(`kubectl api-resources --no-headers -o name | grep -Eq '${regex}'`);
    } catch (error) {
        logger.warn(`Resource type '${resource}' not found in cluster, skipping deletion ${error}`);
        return '';
    }
    const command = `kubectl delete ${resource} -l ${label} -n ${namespace} --ignore-not-found=true --wait=true --timeout=${timeout} ${force ? '--force' : ''}`;
    logger.info(`command: ${command}`);
    const { stdout } = await execAsync(command);
    return stdout;
}
export async function waitForResourceByLabel({ resource, label, namespace, condition = 'Ready', timeout = '10m' }) {
    const command = `kubectl wait ${resource} -l ${label} --for=condition=${condition} -n ${namespace} --timeout=${timeout}`;
    logger.info(`command: ${command}`);
    const { stdout } = await execAsync(command);
    return stdout;
}
export async function waitForResourceByName({ resource, name, namespace, condition = 'Ready', timeout = '10m' }) {
    const command = `kubectl wait ${resource}/${name} --for=condition=${condition} -n ${namespace} --timeout=${timeout}`;
    logger.info(`command: ${command}`);
    const { stdout } = await execAsync(command);
    return stdout;
}
export async function waitForResourcesByName({ resource, names, namespace, condition = 'Ready', timeout = '10m' }) {
    if (!names.length) {
        throw new Error(`No ${resource} names provided to waitForResourcesByName`);
    }
    // Wait all in parallel; if any fails, surface which one.
    await Promise.all(names.map(async (name)=>{
        try {
            await waitForResourceByName({
                resource,
                name,
                namespace,
                condition,
                timeout
            });
        } catch (err) {
            throw new Error(`Failed waiting for ${resource}/${name} condition=${condition} timeout=${timeout} namespace=${namespace}: ${String(err)}`);
        }
    }));
}
/**
 * Waits for all StatefulSets matching a label to have all their replicas ready.
 *
 * @param namespace - Kubernetes namespace
 * @param label - Label selector for StatefulSets (e.g., "app.kubernetes.io/component=sequencer-node")
 * @param timeoutSeconds - Maximum time to wait in seconds
 * @param pollIntervalSeconds - How often to check status
 */ export async function waitForStatefulSetsReady({ namespace, label, timeoutSeconds = 600, pollIntervalSeconds = 5 }) {
    logger.info(`Waiting for StatefulSets with label ${label} to have all replicas ready (timeout: ${timeoutSeconds}s)`);
    await retryUntil(async ()=>{
        // Get all StatefulSets matching the label
        const getCmd = `kubectl get statefulset -l ${label} -n ${namespace} -o json`;
        const { stdout } = await execAsync(getCmd);
        const result = JSON.parse(stdout);
        if (!result.items || result.items.length === 0) {
            logger.verbose(`No StatefulSets found with label ${label}`);
            return false;
        }
        // Check each StatefulSet
        for (const sts of result.items){
            const name = sts.metadata.name;
            const desired = sts.spec.replicas ?? 0;
            const ready = sts.status.readyReplicas ?? 0;
            const updated = sts.status.updatedReplicas ?? 0;
            if (ready < desired || updated < desired) {
                logger.verbose(`StatefulSet ${name}: ${ready}/${desired} ready, ${updated}/${desired} updated`);
                return false;
            }
        }
        logger.info(`All StatefulSets with label ${label} are ready`);
        return true;
    }, `StatefulSets with label ${label} to be ready`, timeoutSeconds, pollIntervalSeconds);
}
/**
 * Creates a Prometheus connection that can re-establish port-forward on failure.
 * Returns functions to connect and run alert checks that automatically reconnect if needed.
 *
 * @param namespace - K8s namespace to fall back to if metrics namespace doesn't have Prometheus
 * @param endpoints - Array to track created endpoints for cleanup
 * @param log - Logger instance
 */ export function createResilientPrometheusConnection(namespace, endpoints, log) {
    let alertChecker;
    let currentEndpoint;
    const connect = async ()=>{
        // Kill existing connection if any
        if (currentEndpoint?.process) {
            currentEndpoint.process.kill();
        }
        // Try metrics namespace first, then network namespace
        let promPort = 0;
        let promUrl = '';
        let promProc;
        try {
            const metricsResult = await startPortForward({
                resource: `svc/metrics-prometheus-server`,
                namespace: 'metrics',
                containerPort: 80
            });
            promProc = metricsResult.process;
            promPort = metricsResult.port;
            promUrl = `http://127.0.0.1:${promPort}/api/v1`;
        } catch  {
            // Metrics namespace might not have Prometheus, try network namespace
            log.verbose('Metrics namespace Prometheus not available, trying network namespace');
        }
        if (promPort === 0) {
            const nsResult = await startPortForward({
                resource: `svc/prometheus-server`,
                namespace,
                containerPort: 80
            });
            promProc = nsResult.process;
            promPort = nsResult.port;
            promUrl = `http://127.0.0.1:${promPort}/api/v1`;
        }
        if (!promProc || promPort === 0) {
            throw new Error('Unable to port-forward to Prometheus');
        }
        currentEndpoint = {
            url: promUrl,
            process: promProc
        };
        endpoints.push(currentEndpoint);
        alertChecker = new GrafanaClient(log, {
            grafanaEndpoint: promUrl,
            grafanaCredentials: ''
        });
        log.info(`Established Prometheus connection at ${promUrl}`);
        return alertChecker;
    };
    const runAlertCheck = async (alerts)=>{
        if (!alertChecker) {
            alertChecker = await connect();
        }
        try {
            await alertChecker.runAlertCheck(alerts);
        } catch (err) {
            // If it's an AlertTriggeredError (expected behavior)
            if (err instanceof AlertTriggeredError) {
                throw err;
            }
            // Check if it's a connection error (port-forward died)
            const errorStr = String(err);
            if (errorStr.includes('fetch failed') || errorStr.includes('ECONNREFUSED') || errorStr.includes('ECONNRESET')) {
                log.warn(`Prometheus connection lost, re-establishing port-forward...`);
                alertChecker = await connect();
                await alertChecker.runAlertCheck(alerts);
            } else {
                throw err;
            }
        }
    };
    return {
        connect,
        runAlertCheck
    };
}
/** Scales the prover-agent Deployment to the given number of replicas. */ export async function scaleProverAgents(namespace, replicas, log) {
    const label = 'app.kubernetes.io/component=prover-agent';
    const command = `kubectl scale deployment -l ${label} -n ${namespace} --replicas=${replicas} --timeout=2m`;
    log.info(`Scaling prover agents to ${replicas}: ${command}`);
    await execAsync(command);
}
export function getChartDir(spartanDir, chartName) {
    return path.join(spartanDir.trim(), chartName);
}
