import { Dt as childLogger, y as markChangesPushed } from "./module-CkyheTUC.js";
import { execFile, execFileSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { promisify } from "node:util";
//#region processors/vetra-cloud-environment/gitops.ts
const execFileAsync = promisify(execFile);
const logger$1 = childLogger(["gitops"]);
var GitMutex = class {
	queue = [];
	locked = false;
	async acquire() {
		if (!this.locked) {
			this.locked = true;
			return;
		}
		return new Promise((resolve) => {
			this.queue.push(resolve);
		});
	}
	release() {
		const next = this.queue.shift();
		if (next) next();
		else this.locked = false;
	}
};
const gitMutex = new GitMutex();
const MAX_PUSH_RETRIES = 3;
const GIT_AUTHOR_NAME = process.env.GITOPS_AUTHOR_NAME ?? "vetra-cloud-processor";
const GIT_AUTHOR_EMAIL = process.env.GITOPS_AUTHOR_EMAIL ?? "noreply@vetra.io";
function getConfig() {
	return {
		repoUrl: getRepoUrl(),
		remote: process.env.GITOPS_REMOTE ?? "origin",
		branch: process.env.GITOPS_BRANCH ?? "main"
	};
}
/**
* Resolve the remote repo URL.
*
* Supports two env-var modes:
*  1. GITOPS_REPO_URL  — explicit URL (preferred in production)
*  2. GITOPS_REPO_PATH — local checkout; we read the remote URL from it
*     (backwards-compatible with existing .env files)
*
* When GITOPS_GITHUB_PAT is set, injects the token into https:// URLs so
* ephemeral clones can authenticate.
*/
function getRepoUrl() {
	let url = process.env.GITOPS_REPO_URL;
	if (!url) {
		const repoPath = process.env.GITOPS_REPO_PATH;
		if (!repoPath) throw new Error("Either GITOPS_REPO_URL or GITOPS_REPO_PATH environment variable is required");
		url = execFileSync("git", [
			"-C",
			repoPath,
			"remote",
			"get-url",
			process.env.GITOPS_REMOTE ?? "origin"
		], {
			encoding: "utf-8",
			timeout: 1e4
		}).trim();
	}
	const pat = process.env.GITOPS_GITHUB_PAT;
	if (pat && url.startsWith("https://")) {
		const parsed = new URL(url);
		parsed.username = pat;
		parsed.password = "x-oauth-basic";
		url = parsed.toString();
	}
	return url;
}
/**
* Redact credentials from a URL for safe logging.
*/
function redactUrl(url) {
	try {
		const parsed = new URL(url);
		if (parsed.username || parsed.password) {
			parsed.username = "***";
			parsed.password = "***";
			return parsed.toString();
		}
		return url;
	} catch {
		return url;
	}
}
async function git(args, cwd) {
	const safeArgs = args.map((arg) => arg.startsWith("https://") ? redactUrl(arg) : arg);
	logger$1.info(`Running: git ${safeArgs.join(" ")}`);
	const { stdout } = await execFileAsync("git", args, {
		cwd,
		encoding: "utf-8",
		timeout: 6e4
	});
	return stdout.trim();
}
function toKebabCase(name) {
	return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
}
/**
* Build a unique infrastructure tenant ID.
*
* Format: `<subdomain>-<shortDocId>`
*
* The subdomain alone can collide (250k namespace), so we append the first 8
* hex chars of the documentId to guarantee uniqueness.
*/
function getTenantId(subdomain, documentId) {
	return `${subdomain}-${documentId.replace(/-/g, "").slice(0, 8)}`;
}
/**
* Escape a string for safe interpolation into a YAML value.
* Wraps in double quotes and escapes inner backslashes, double quotes,
* and newlines.
*/
function yamlQuote(value) {
	return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
}
function generateCustomDomainIngress(service, customDomain) {
	const host = service === "switchboard" ? customDomain : `${service}.${customDomain}`;
	const secretSuffix = customDomain.replace(/\./g, "-");
	return `
  additionalIngresses:
    - enabled: true
      className: traefik
      host: ${yamlQuote(host)}
      tls:
        enabled: true
        secretName: ${service}-custom-${secretSuffix}-tls
      annotations:
        cert-manager.io/cluster-issuer: letsencrypt-prod`;
}
const DEFAULT_IMAGE_TAG = "v6.0.0-dev.152";
function generateValuesYaml(state, documentId) {
	const subdomain = state.genericSubdomain;
	const tenantId = getTenantId(subdomain, documentId);
	const name = toKebabCase(state.label ?? "unnamed");
	const switchboardEnabled = state.services.some((s) => s.type === "SWITCHBOARD" && s.enabled);
	const connectEnabled = state.services.some((s) => s.type === "CONNECT" && s.enabled);
	const switchboardService = state.services.find((s) => s.type === "SWITCHBOARD");
	const connectService = state.services.find((s) => s.type === "CONNECT");
	const switchboardTag = switchboardService?.version ?? DEFAULT_IMAGE_TAG;
	const connectTag = connectService?.version ?? DEFAULT_IMAGE_TAG;
	const disabled = new Set([
		"TERMINATING",
		"DESTROYED",
		"ARCHIVED"
	]).has(state.status);
	const phPackages = state.packages?.map((p) => `${p.name}@${p.version ?? "latest"}`).join(",") ?? "";
	const customDomain = state.customDomain?.enabled ? state.customDomain.domain ?? null : null;
	const switchboardCustomIngress = customDomain && switchboardEnabled ? generateCustomDomainIngress("switchboard", customDomain) : "";
	const connectCustomIngress = customDomain && connectEnabled ? generateCustomDomainIngress("connect", customDomain) : "";
	const tenantName = yamlQuote(state.label ?? name);
	const dbName = tenantId.replace(/-/g, "_");
	return `global:
  disabled: ${disabled}
  subdomain: ${yamlQuote(subdomain)}
  imagePullSecrets:
    enabled: true
    name: harbor-credentials
    useExisting: true
database:
  cnpg:
    enabled: true
    name: ${tenantId}-pg
    instances: 1
    storageClass: hcloud-volumes
    storageSize: 50Gi
    postgresql:
      maxConnections: "600"
      sharedBuffers: 512MB
      effectiveCacheSize: 2GB
      workMem: 32MB
    pooler:
      enabled: true
      instances: 1
      poolMode: transaction
      defaultPoolSize: 50
      maxClientConnections: 400
    backup:
      enabled: true
      destinationPath: s3://powerhouse-cnpg-backups/${tenantId}/
      endpointURL: https://hel1.your-objectstorage.com
      credentialsSecret: s3-credentials
      retentionPolicy: 180d
      useExistingSecret: true
      scheduledBackup:
        enabled: true
        schedule: 0 2 * * *
        immediate: true
    resources:
      requests:
        memory: 2Gi
        cpu: "2"
      limits:
        memory: 8Gi
        cpu: "8"
    bootstrap:
      database: ${dbName}_db
      owner: ${dbName}_user
switchboard:
  enabled: ${switchboardEnabled}
  gitops:
    enabled: false
  name: switchboard
  replicaCount: 1
  image:
    repository: cr.vetra.io/powerhouse-inc-powerhouse/switchboard
    tag: ${switchboardTag}
    pullPolicy: IfNotPresent
  service:
    type: ClusterIP
    port: 80
    targetPort: 3000
  ingress:
    enabled: true
    className: traefik
    tls:
      enabled: true
      secretName: switchboard-${subdomain}-vetra-io-tls
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod${switchboardCustomIngress}
  env:
    PORT: "3000"
    NODE_ENV: production
    PH_REGISTRY_URL: ${yamlQuote(state.defaultPackageRegistry || "https://registry.dev.vetra.io")}
    PH_REGISTRY_PACKAGES: ${yamlQuote(phPackages)}
    OPENBAO_ADDR: https://openbao.vetra.io
    PROMETHEUS_URL: http://prometheus-server.monitoring.svc
    LOKI_URL: http://loki.monitoring.svc:3100
  envConfigMap:
    TENANT_ID: ${tenantId}
    TENANT_NAME: ${tenantName}
  livenessProbe:
    enabled: true
    exec:
      command:
        - /bin/sh
        - -c
        - |
          wget --post-data='{"query":"{__typename}"}' --header='Content-Type: application/json' -qO- http://localhost:3000/graphql
    initialDelaySeconds: 120
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 6
  readinessProbe:
    enabled: true
    exec:
      command:
        - /bin/sh
        - -c
        - |
          wget --post-data='{"query":"{__typename}"}' --header='Content-Type: application/json' -qO- http://localhost:3000/graphql
    initialDelaySeconds: 60
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 6
  securityContext:
    runAsNonRoot: false
    runAsUser: 0
    fsGroup: 0
  autoscaling:
    enabled: false
connect:
  enabled: ${connectEnabled}
  name: connect
  replicaCount: 1
  image:
    repository: cr.vetra.io/powerhouse-inc-powerhouse/connect
    tag: ${connectTag}
    pullPolicy: IfNotPresent
  service:
    type: ClusterIP
    port: 80
    targetPort: 3001
  ingress:
    enabled: true
    className: traefik
    tls:
      enabled: true
      secretName: connect-${subdomain}-vetra-io-tls
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod${connectCustomIngress}
  env:
    PORT: "3001"
    NODE_ENV: production
    PH_REGISTRY_URL: ${yamlQuote(state.defaultPackageRegistry || "https://registry.dev.vetra.io")}
    PH_REGISTRY_PACKAGES: ${yamlQuote(phPackages)}
  envConfigMap:
    TENANT_ID: ${tenantId}
    TENANT_NAME: ${tenantName}
  livenessProbe:
    enabled: true
    httpGet:
      path: /health
      port: 3001
    initialDelaySeconds: 120
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 6
  readinessProbe:
    enabled: true
    httpGet:
      path: /health
      port: 3001
    initialDelaySeconds: 60
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 6
  securityContext:
    runAsNonRoot: false
    runAsUser: 0
    fsGroup: 0
  autoscaling:
    enabled: false
podDisruptionBudget:
  enabled: true
  minAvailable: 1
serviceMonitor:
  enabled: true
  interval: 30s
  path: /metrics
glitchtip:
  enabled: false
  environment: ${tenantId}
networkPolicy:
  enabled: false
`;
}
/**
* Sync an environment's values to the gitops repo.
*
* Each call creates a fresh shallow clone, writes the values file, commits,
* and pushes. The clone is always cleaned up afterwards. This eliminates all
* shared working-tree state issues.
*
* A mutex serializes syncs within the same process to avoid redundant clones.
* Cross-process / cross-pod push races are handled by pull-rebase-retry.
*/
async function syncEnvironment(state, documentId) {
	if (!state.label) {
		logger$1.warn("Environment has no label, skipping gitops sync");
		return;
	}
	if (!state.genericSubdomain) {
		logger$1.warn(`Environment "${state.label}" has no subdomain, skipping gitops sync`);
		return;
	}
	await gitMutex.acquire();
	try {
		await syncEnvironmentEphemeral(state, documentId);
	} finally {
		gitMutex.release();
	}
}
/**
* Remove a tenant's directory from the gitops repo.
*
* Called when an environment document is deleted. Removes the tenant
* directory, commits, and pushes so ArgoCD/Flux tears down the deployment.
*/
async function deleteEnvironmentFromGitops(tenantId) {
	await gitMutex.acquire();
	try {
		await withEphemeralClone(async (cloneDir, config) => {
			if (!existsSync(join(cloneDir, "tenants", tenantId))) {
				logger$1.info(`Tenant directory "tenants/${tenantId}" does not exist in gitops repo, nothing to remove`);
				return;
			}
			await git([
				"rm",
				"-r",
				`tenants/${tenantId}`
			], cloneDir);
			const commitMsg = `chore(${tenantId}): remove tenant — environment deleted`;
			logger$1.info(`Committing: ${commitMsg}`);
			await git([
				"commit",
				"-m",
				commitMsg
			], cloneDir);
			await pushWithRetry(cloneDir, config);
			logger$1.info(`Successfully removed tenant "${tenantId}" from gitops repo`);
		});
	} finally {
		gitMutex.release();
	}
}
async function syncEnvironmentEphemeral(state, documentId) {
	const subdomain = state.genericSubdomain;
	const tenantId = getTenantId(subdomain, documentId);
	logger$1.info(`Syncing environment "${tenantId}" (subdomain=${subdomain})`);
	logger$1.info(`Environment state: status=${state.status}, services=[${state.services?.map((s) => `${s.type}:${s.enabled}`).join(", ")}], packages=[${state.packages?.map((p) => `${p.name}@${p.version}`).join(", ") ?? ""}], subdomain=${subdomain}, customDomain=${state.customDomain?.domain ?? "unset"}`);
	await withEphemeralClone(async (cloneDir, config) => {
		const tenantDir = join(cloneDir, "tenants", tenantId);
		mkdirSync(tenantDir, { recursive: true });
		const valuesPath = join(tenantDir, "powerhouse-values.yaml");
		writeFileSync(valuesPath, generateValuesYaml(state, documentId), "utf-8");
		logger$1.info(`Wrote values file to ${valuesPath}`);
		await git(["add", `tenants/${tenantId}/powerhouse-values.yaml`], cloneDir);
		const hasChanges = await git([
			"diff",
			"--cached",
			"--name-only"
		], cloneDir);
		if (!hasChanges) {
			logger$1.info("No changes detected in values file, skipping commit");
			return;
		}
		logger$1.info(`Changes detected: ${hasChanges}`);
		const commitMsg = `chore(${tenantId}): ${state.status === "READY" ? "enable" : "disable"} tenant — synced from vetra-cloud-environment`;
		logger$1.info(`Committing: ${commitMsg}`);
		await git([
			"commit",
			"-m",
			commitMsg
		], cloneDir);
		await pushWithRetry(cloneDir, config);
		logger$1.info(`Successfully synced and pushed environment "${tenantId}"`);
	});
}
async function withEphemeralClone(fn) {
	const config = getConfig();
	const cloneDir = mkdtempSync(join(tmpdir(), "gitops-"));
	logger$1.info(`Cloning into ephemeral directory: ${cloneDir}`);
	try {
		await git([
			"clone",
			"--depth",
			"1",
			"--branch",
			config.branch,
			config.repoUrl,
			"."
		], cloneDir);
		await git([
			"config",
			"user.name",
			GIT_AUTHOR_NAME
		], cloneDir);
		await git([
			"config",
			"user.email",
			GIT_AUTHOR_EMAIL
		], cloneDir);
		await fn(cloneDir, config);
	} finally {
		logger$1.info(`Cleaning up ephemeral clone: ${cloneDir}`);
		try {
			rmSync(cloneDir, {
				recursive: true,
				force: true
			});
		} catch (cleanupError) {
			logger$1.warn(`Failed to clean up ${cloneDir}: ${String(cleanupError)}`);
		}
	}
}
async function pushWithRetry(cloneDir, config) {
	for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) try {
		logger$1.info(`Pushing to ${config.remote}/${config.branch} (attempt ${attempt})...`);
		await git([
			"push",
			config.remote,
			config.branch
		], cloneDir);
		return;
	} catch (error) {
		if (attempt === MAX_PUSH_RETRIES) throw error;
		logger$1.warn(`Push attempt ${attempt} failed, pulling with rebase and retrying: ${String(error)}`);
		await git([
			"fetch",
			config.remote,
			config.branch
		], cloneDir);
		try {
			await git(["rebase", `${config.remote}/${config.branch}`], cloneDir);
		} catch (rebaseError) {
			logger$1.warn(`Rebase failed with conflict, aborting: ${String(rebaseError)}`);
			await git(["rebase", "--abort"], cloneDir);
			throw rebaseError;
		}
	}
}
//#endregion
//#region processors/vetra-cloud-environment/index.ts
const logger = childLogger(["vetra-cloud-environment-processor"]);
var VetraCloudEnvironmentProcessor = class {
	relationalDb;
	reactorClient;
	constructor(relationalDb, reactorClient) {
		this.relationalDb = relationalDb;
		this.reactorClient = reactorClient;
	}
	async dispatchAction(documentId, action) {
		if (!this.reactorClient) {
			logger.warn(`Cannot dispatch ${action.type}: no reactor client`);
			return;
		}
		try {
			await this.reactorClient.execute(documentId, "main", [action]);
			logger.info(`Dispatched ${action.type} for ${documentId}`);
		} catch (err) {
			logger.error(`Failed to dispatch ${action.type} for ${documentId}: ${String(err)}`);
		}
	}
	async onOperations(operations) {
		if (operations.length === 0) return;
		logger.info(`Received ${operations.length} operations`);
		const deletedIds = /* @__PURE__ */ new Set();
		for (const { operation, context } of operations) if (context.documentType === "powerhouse/document-drive") {
			const deletedId = await this.handleDriveOperation(operation);
			if (deletedId) deletedIds.add(deletedId);
		}
		const lastByDocument = /* @__PURE__ */ new Map();
		for (const entry of operations) if (entry.context.documentType === "powerhouse/vetra-cloud-environment" && !deletedIds.has(entry.context.documentId)) lastByDocument.set(entry.context.documentId, entry);
		if (lastByDocument.size > 0) logger.info(`Processing ${lastByDocument.size} environment(s) (deduplicated from ${operations.filter((o) => o.context.documentType === "powerhouse/vetra-cloud-environment").length} operations)`);
		for (const [documentId, { operation, context }] of lastByDocument) {
			let phState = context.resultingState ? JSON.parse(context.resultingState) : void 0;
			if (!phState && this.reactorClient) try {
				phState = (await this.reactorClient.get(documentId)).state;
				if (phState) logger.info(`Fetched current state for ${documentId} (resultingState was missing)`);
			} catch (err) {
				logger.warn(`Failed to fetch state for ${documentId}: ${String(err)}`);
			}
			if (!phState) {
				logger.warn(`No state available for operation ${operation.index} on ${documentId}`);
				continue;
			}
			const state = phState.global;
			const { label: envLabel, genericSubdomain, customDomain, packages, services, status } = state;
			const label = envLabel ?? documentId;
			const tenantId = genericSubdomain ? getTenantId(genericSubdomain, documentId) : null;
			logger.info(`Processing document ${label} (op ${operation.index}): label=${envLabel ?? "unset"}, status=${status ?? "unset"}, subdomain=${genericSubdomain ?? "unset"}, tenantId=${tenantId ?? "unset"}, services=[${services?.map((s) => `${s.type}:${s.enabled}`).join(", ") ?? ""}], packages=[${packages?.map((p) => `${p.name}@${p.version}`).join(", ") ?? ""}]`);
			const row = {
				name: envLabel ?? null,
				subdomain: genericSubdomain ?? null,
				tenantId,
				customDomain: customDomain?.domain ?? null,
				packages: JSON.stringify(packages ?? []),
				services: JSON.stringify(services ?? []),
				status: status ?? null
			};
			logger.info(`Upserting environment record for "${label}"`);
			await this.relationalDb.insertInto("environments").values({
				id: documentId,
				...row
			}).onConflict((oc) => oc.column("id").doUpdateSet(row)).execute();
			if (status === "CHANGES_APPROVED") {
				logger.info(`Triggering gitops sync for "${label}"`);
				try {
					await syncEnvironment(state, documentId);
					logger.info(`Gitops sync completed for "${label}"`);
					if (this.reactorClient) {
						const freshStatus = (await this.reactorClient.get(documentId))?.state?.global?.status;
						if (freshStatus !== "CHANGES_APPROVED") logger.info(`Skipping MARK_CHANGES_PUSHED for "${label}" — status already changed to ${freshStatus}`);
						else await this.dispatchAction(documentId, markChangesPushed({}));
					} else await this.dispatchAction(documentId, markChangesPushed({}));
				} catch (error) {
					logger.error(`Gitops sync failed for "${label}": ${String(error)}`);
				}
			} else logger.info(`Skipping gitops sync for "${label}" (status: ${status})`);
		}
	}
	/**
	* Handle a drive-level operation. Returns the deleted document ID if a
	* DELETE_NODE was processed, so the caller can skip syncing it.
	*/
	async handleDriveOperation(operation) {
		if (operation.action.type !== "DELETE_NODE") return void 0;
		const deletedNodeId = operation.action.input?.id;
		if (!deletedNodeId) return void 0;
		const environment = await this.relationalDb.selectFrom("environments").select([
			"id",
			"name",
			"subdomain"
		]).where("id", "=", deletedNodeId).executeTakeFirst();
		if (!environment) return deletedNodeId;
		const label = environment.name ?? deletedNodeId;
		logger.info(`Deleting environment record for "${label}"`);
		await this.relationalDb.deleteFrom("environments").where("id", "=", deletedNodeId).execute();
		if (environment.subdomain) {
			const tenantId = getTenantId(environment.subdomain, deletedNodeId);
			logger.info(`Removing gitops tenant "${tenantId}" for deleted environment "${label}"`);
			try {
				await deleteEnvironmentFromGitops(tenantId);
				logger.info(`Gitops cleanup completed for "${label}"`);
			} catch (error) {
				logger.error(`Gitops cleanup failed for "${label}": ${String(error)}`);
			}
		}
		return deletedNodeId;
	}
	async onDisconnect() {}
};
//#endregion
export { VetraCloudEnvironmentProcessor as t };

//# sourceMappingURL=vetra-cloud-environment-YyVSJKe9.js.map