// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
export function classifyPodService(podName) {
    if (podName.includes("connect"))
        return "CONNECT";
    if (podName.includes("switchboard"))
        return "SWITCHBOARD";
    return "OTHER";
}
/** Uppercase enum values to match GraphQL schema (K8s returns Mixed case).
 *  Handles camelCase (e.g. OutOfSync → OUT_OF_SYNC) and spaces. */
function toUpperEnum(val) {
    return val
        .replace(/([a-z])([A-Z])/g, "$1_$2")
        .toUpperCase()
        .replace(/ /g, "_");
}
// ---------------------------------------------------------------------------
// DB helpers
// ---------------------------------------------------------------------------
export async function upsertEnvironmentStatus(db, row) {
    await db
        .insertInto("environment_status")
        .values(row)
        .onConflict((oc) => oc.column("tenantId").doUpdateSet({
        argoSyncStatus: row.argoSyncStatus,
        argoHealthStatus: row.argoHealthStatus,
        argoLastSyncedAt: row.argoLastSyncedAt,
        argoMessage: row.argoMessage,
        configDriftDetected: row.configDriftDetected,
        tlsCertValid: row.tlsCertValid,
        tlsCertExpiresAt: row.tlsCertExpiresAt,
        domainResolves: row.domainResolves,
        updatedAt: row.updatedAt,
    }))
        .execute();
}
export async function upsertPod(db, row) {
    await db
        .insertInto("environment_pods")
        .values(row)
        .onConflict((oc) => oc.column("id").doUpdateSet({
        phase: row.phase,
        ready: row.ready,
        restartCount: row.restartCount,
        service: row.service,
        updatedAt: row.updatedAt,
    }))
        .execute();
}
export async function insertEvent(db, row) {
    await db
        .insertInto("environment_events")
        .values(row)
        .onConflict((oc) => oc.column("id").doNothing())
        .execute();
}
export async function pruneEvents(db, tenantId, keepCount = 50) {
    const topIds = await db
        .selectFrom("environment_events")
        .select("id")
        .where("tenantId", "=", tenantId)
        .orderBy("timestamp", "desc")
        .limit(keepCount)
        .execute();
    if (topIds.length < keepCount)
        return;
    await db
        .deleteFrom("environment_events")
        .where("tenantId", "=", tenantId)
        .where("id", "not in", topIds.map((r) => r.id))
        .execute();
}
function watchWithReconnect(watch, path, queryParams, onEvent, label) {
    let active = true;
    let consecutiveFailures = 0;
    let requestHandle = null;
    function start() {
        if (!active)
            return;
        watch
            .watch(path, queryParams, (phase, obj) => {
            consecutiveFailures = 0;
            onEvent(phase, obj).catch((err) => {
                console.warn(`[watcher:${label}] event handler error`, err);
            });
        }, (err) => {
            if (!active)
                return;
            if (err) {
                consecutiveFailures++;
                console.warn(`[watcher:${label}] watch error (consecutive=${consecutiveFailures})`, err);
            }
            if (consecutiveFailures >= 3) {
                console.warn(`[watcher:${label}] 3 consecutive failures, stopping watcher`);
                active = false;
                return;
            }
            // reconnect
            start();
        })
            .then((handle) => {
            requestHandle = handle;
        })
            .catch((err) => {
            if (!active)
                return;
            consecutiveFailures++;
            console.warn(`[watcher:${label}] failed to start watch`, err);
            if (consecutiveFailures >= 3) {
                console.warn(`[watcher:${label}] 3 consecutive failures, stopping watcher`);
                active = false;
                return;
            }
            start();
        });
    }
    start();
    return {
        abort() {
            active = false;
            requestHandle?.abort();
        },
    };
}
// ---------------------------------------------------------------------------
// Reconciliation (lists current state and upserts to DB)
// ---------------------------------------------------------------------------
async function reconcile(db, k8sToken, k8sApiUrl) {
    try {
        const { KubeConfig, CustomObjectsApi, CoreV1Api } = await import("@kubernetes/client-node");
        const kc = new KubeConfig();
        if (k8sToken) {
            const server = k8sApiUrl || "https://kubernetes.default.svc";
            kc.loadFromOptions({
                clusters: [{ name: "cluster", server, skipTLSVerify: true }],
                users: [{ name: "user", token: k8sToken }],
                contexts: [{ name: "ctx", cluster: "cluster", user: "user" }],
                currentContext: "ctx",
            });
        }
        else {
            kc.loadFromCluster();
        }
        const customApi = kc.makeApiClient(CustomObjectsApi);
        const coreApi = kc.makeApiClient(CoreV1Api);
        // List ArgoCD apps
        const appsResponse = await customApi.listClusterCustomObject({
            group: "argoproj.io",
            version: "v1alpha1",
            plural: "applications",
        });
        const appsBody = appsResponse;
        for (const app of appsBody.items ?? []) {
            const tenantId = app.metadata?.labels?.["tenant"];
            if (!tenantId)
                continue;
            const syncStatus = app.status?.sync?.status ?? "Unknown";
            const healthStatus = app.status?.health?.status ?? "Unknown";
            const lastSyncedAt = app.status?.operationState?.finishedAt ?? null;
            const message = app.status?.health?.message ??
                app.status?.operationState?.message ??
                null;
            const driftDetected = (app.status?.conditions ?? []).some((c) => c.type === "ComparisonError")
                ? 1
                : 0;
            await upsertEnvironmentStatus(db, {
                tenantId,
                argoSyncStatus: toUpperEnum(syncStatus),
                argoHealthStatus: toUpperEnum(healthStatus),
                argoLastSyncedAt: lastSyncedAt,
                argoMessage: message,
                configDriftDetected: driftDetected,
                tlsCertValid: null,
                tlsCertExpiresAt: null,
                domainResolves: null,
                updatedAt: new Date().toISOString(),
            });
        }
        // List all pods across all namespaces (filtered by tenant namespace in DB)
        const podsResponse = await coreApi.listPodForAllNamespaces({});
        const podsBody = podsResponse;
        for (const pod of podsBody.items ?? []) {
            const tenantId = pod.metadata?.namespace;
            const name = pod.metadata?.name;
            if (!tenantId || !name)
                continue;
            const containerStatuses = pod.status?.containerStatuses ?? [];
            const ready = containerStatuses.every((cs) => cs.ready) ? 1 : 0;
            const restartCount = containerStatuses.reduce((sum, cs) => sum + (cs.restartCount ?? 0), 0);
            await upsertPod(db, {
                id: `${tenantId}/${name}`,
                tenantId,
                name,
                service: classifyPodService(name),
                phase: toUpperEnum(pod.status?.phase ?? "Unknown"),
                ready,
                restartCount,
                updatedAt: new Date().toISOString(),
            });
        }
    }
    catch (err) {
        console.warn("[reconcile] error during reconciliation", err);
    }
}
// ---------------------------------------------------------------------------
// startWatchers
// ---------------------------------------------------------------------------
export function startWatchers(deps) {
    const { db, k8sToken, k8sApiUrl = "" } = deps;
    const watcherAborts = [];
    // Reconciliation interval (60 seconds)
    const intervalId = setInterval(() => {
        void reconcile(db, k8sToken, k8sApiUrl);
    }, 60_000);
    // Run an initial reconcile
    void reconcile(db, k8sToken, k8sApiUrl);
    // Start K8s watches via dynamic import so tests don't require a live cluster
    (async () => {
        try {
            const { KubeConfig, Watch } = await import("@kubernetes/client-node");
            const kc = new KubeConfig();
            if (k8sToken) {
                const server = k8sApiUrl || "https://kubernetes.default.svc";
                kc.loadFromOptions({
                    clusters: [{ name: "cluster", server, skipTLSVerify: true }],
                    users: [{ name: "user", token: k8sToken }],
                    contexts: [{ name: "ctx", cluster: "cluster", user: "user" }],
                    currentContext: "ctx",
                });
            }
            else {
                // Use in-cluster SA token (mounted at /var/run/secrets/...)
                kc.loadFromCluster();
            }
            const watch = new Watch(kc);
            // ArgoCD applications watcher
            const argoAbort = watchWithReconnect(watch, "/apis/argoproj.io/v1alpha1/applications", {}, async (_phase, obj) => {
                const app = obj;
                const tenantId = app.metadata?.labels?.["tenant"];
                if (!tenantId)
                    return;
                const syncStatus = app.status?.sync?.status ?? "Unknown";
                const healthStatus = app.status?.health?.status ?? "Unknown";
                const lastSyncedAt = app.status?.operationState?.finishedAt ?? null;
                const message = app.status?.health?.message ??
                    app.status?.operationState?.message ??
                    null;
                const driftDetected = (app.status?.conditions ?? []).some((c) => c.type === "ComparisonError")
                    ? 1
                    : 0;
                await upsertEnvironmentStatus(db, {
                    tenantId,
                    argoSyncStatus: toUpperEnum(syncStatus),
                    argoHealthStatus: toUpperEnum(healthStatus),
                    argoLastSyncedAt: lastSyncedAt,
                    argoMessage: message,
                    configDriftDetected: driftDetected,
                    tlsCertValid: null,
                    tlsCertExpiresAt: null,
                    domainResolves: null,
                    updatedAt: new Date().toISOString(),
                });
                // Also record sync/health changes as deployment events
                const eventType = healthStatus === "Degraded" || healthStatus === "Missing"
                    ? "WARNING"
                    : "NORMAL";
                const reason = syncStatus === "Synced" && healthStatus === "Healthy"
                    ? "SyncSucceeded"
                    : syncStatus === "OutOfSync"
                        ? "SyncOutOfSync"
                        : `${syncStatus}/${healthStatus}`;
                await insertEvent(db, {
                    id: `argo-${tenantId}-${Date.now()}`,
                    tenantId,
                    type: eventType,
                    reason,
                    message: message ?? `Sync: ${syncStatus}, Health: ${healthStatus}`,
                    involvedObject: `Application/${tenantId}`,
                    timestamp: lastSyncedAt ?? new Date().toISOString(),
                });
                await pruneEvents(db, tenantId);
            }, "argo-apps");
            watcherAborts.push(argoAbort);
            // Pod watcher
            const podAbort = watchWithReconnect(watch, "/api/v1/pods", {}, async (_phase, obj) => {
                const pod = obj;
                const tenantId = pod.metadata?.namespace;
                const name = pod.metadata?.name;
                if (!tenantId || !name)
                    return;
                const containerStatuses = pod.status?.containerStatuses ?? [];
                const ready = containerStatuses.every((cs) => cs.ready) ? 1 : 0;
                const restartCount = containerStatuses.reduce((sum, cs) => sum + (cs.restartCount ?? 0), 0);
                await upsertPod(db, {
                    id: `${tenantId}/${name}`,
                    tenantId,
                    name,
                    service: classifyPodService(name),
                    phase: toUpperEnum(pod.status?.phase ?? "Unknown"),
                    ready,
                    restartCount,
                    updatedAt: new Date().toISOString(),
                });
            }, "pods");
            watcherAborts.push(podAbort);
            // Event watcher
            const eventAbort = watchWithReconnect(watch, "/api/v1/events", {}, async (_phase, obj) => {
                const event = obj;
                const id = event.metadata?.uid;
                const tenantId = event.involvedObject?.namespace;
                if (!id || !tenantId)
                    return;
                const involvedObject = event.involvedObject
                    ? `${event.involvedObject.kind ?? ""}/${event.involvedObject.name ?? ""}`
                    : "";
                await insertEvent(db, {
                    id,
                    tenantId,
                    type: toUpperEnum(event.type ?? "Normal"),
                    reason: event.reason ?? "",
                    message: event.message ?? "",
                    involvedObject,
                    timestamp: event.lastTimestamp ??
                        event.eventTime ??
                        new Date().toISOString(),
                });
                await pruneEvents(db, tenantId);
            }, "events");
            watcherAborts.push(eventAbort);
        }
        catch (err) {
            console.warn("[startWatchers] failed to start K8s watches, relying on reconcile loop only", err);
        }
    })();
    return {
        stop() {
            clearInterval(intervalId);
            for (const handle of watcherAborts) {
                handle.abort();
            }
        },
    };
}
