"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var HetznerVolumeOperationsService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HetznerVolumeOperationsService = void 0;
const common_1 = require("@nestjs/common");
const kubernetes_service_1 = require("../../../infrastructure/shared/services/kubernetes.service");
const COPY_POD_IMAGE = 'busybox:1.37';
const SNAPSHOT_LABEL_MARKER = 'flui.cloud/pvc-clone-snapshot';
const SOURCE_PVC_LABEL = 'flui.cloud/snapshot-of';
const COPY_JOB_TIMEOUT_SECONDS = 30 * 60;
/**
 * Hetzner does not expose volume snapshots or volume cloning at the API level
 * (https://docs.hetzner.com/cloud/volumes/, https://github.com/hetznercloud/csi-driver/issues/88).
 * To honor the IVolumeOperations contract on Hetzner we implement "snapshot"
 * as a labeled PVC clone built via the copy-pod pattern: a Job that mounts
 * source + destination PVC on the same node and tars data across.
 *
 * Cost note: each "snapshot" on Hetzner is a full Volume billed at the
 * standard volume rate (€0.057/GB-mo, 10 GB minimum). The capabilities
 * surface this so the CLI/cost estimator can warn users.
 */
let HetznerVolumeOperationsService = HetznerVolumeOperationsService_1 = class HetznerVolumeOperationsService {
    constructor(k8s) {
        this.k8s = k8s;
        this.logger = new common_1.Logger(HetznerVolumeOperationsService_1.name);
        this.capabilities = {
            supportsSnapshotRetention: false,
            supportsCsiSnapshot: false,
            requiresCopyPodFallback: true,
            snapshotCostPerGbMonthEur: null,
        };
    }
    async createSnapshot(input) {
        const { kubeconfig, namespace, sourcePvcName, snapshotName, labels } = input;
        const sourcePvc = await this.k8s.getResource(kubeconfig, 'PersistentVolumeClaim', sourcePvcName, namespace);
        if (!sourcePvc) {
            throw new Error(`Source PVC ${namespace}/${sourcePvcName} not found, cannot snapshot`);
        }
        const sourceUid = sourcePvc?.metadata?.uid;
        const storageRequest = sourcePvc?.spec?.resources?.requests?.storage ?? '10Gi';
        const sizeGb = this.parseStorageGb(storageRequest);
        const storageClass = sourcePvc?.spec?.storageClassName;
        const sourceNode = sourcePvc?.metadata?.annotations?.['volume.kubernetes.io/selected-node'];
        const snapshotPvcLabels = {
            ...labels,
            [SNAPSHOT_LABEL_MARKER]: 'true',
            [SOURCE_PVC_LABEL]: sourcePvcName,
            ...(sourceUid ? { 'flui.cloud/source-pvc-uid': sourceUid } : {}),
        };
        const newPvcManifest = this.renderPvcManifest({
            name: snapshotName,
            namespace,
            storageClassName: storageClass,
            storage: storageRequest,
            labels: snapshotPvcLabels,
        });
        await this.k8s.applyManifest(kubeconfig, newPvcManifest);
        this.logger.log(`[hetzner-snapshot] Created clone PVC ${namespace}/${snapshotName} (size=${sizeGb}Gi, sourceNode=${sourceNode ?? 'n/a'})`);
        const jobName = `${snapshotName}-copy`;
        const jobManifest = this.renderCopyJobManifest({
            jobName,
            namespace,
            sourcePvcName,
            destPvcName: snapshotName,
            nodeSelectorHostname: sourceNode,
            labels: snapshotPvcLabels,
        });
        await this.k8s.applyManifest(kubeconfig, jobManifest);
        this.logger.log(`[hetzner-snapshot] Started copy-pod Job ${namespace}/${jobName}`);
        await this.waitForJobCompletion(kubeconfig, namespace, jobName, COPY_JOB_TIMEOUT_SECONDS);
        await this.cleanupCopyJob(kubeconfig, namespace, jobName);
        return {
            snapshotId: snapshotName,
            kind: 'pvc-clone',
            namespace,
            sourceSizeGb: sizeGb,
            createdAt: new Date().toISOString(),
            ready: true,
        };
    }
    async deleteSnapshot(input) {
        try {
            await this.k8s.deleteResource(input.kubeconfig, 'PersistentVolumeClaim', input.snapshotId, input.namespace);
        }
        catch (err) {
            if (input.ignoreNotFound && err?.message?.includes('not found')) {
                return;
            }
            throw err;
        }
    }
    async listSnapshots(input) {
        const labelSelectorParts = [`${SNAPSHOT_LABEL_MARKER}=true`];
        if (input.labelSelector) {
            labelSelectorParts.push(input.labelSelector);
        }
        const labelSelector = labelSelectorParts.join(',');
        const namespace = input.namespace ?? '';
        if (!namespace) {
            this.logger.debug('[hetzner-snapshot] cluster-wide list requires namespace iteration; returning empty result. Caller should pass per-namespace queries.');
            return [];
        }
        const items = await this.k8s.listResourcesByLabel(input.kubeconfig, 'PersistentVolumeClaim', namespace, labelSelector);
        return items.map((pvc) => {
            const labels = pvc?.metadata?.labels ?? {};
            const sizeGb = this.parseStorageGb(pvc?.spec?.resources?.requests?.storage ?? '0');
            return {
                snapshotId: pvc?.metadata?.name,
                kind: 'pvc-clone',
                namespace: pvc?.metadata?.namespace,
                sourcePvcName: labels[SOURCE_PVC_LABEL],
                appId: labels['flui-app-id'],
                sizeGb,
                createdAt: pvc?.metadata?.creationTimestamp ??
                    new Date().toISOString(),
                ready: pvc?.status?.phase === 'Bound',
                labels,
            };
        });
    }
    async createPvcFromSnapshot(input) {
        const sourcePvc = await this.k8s.getResource(input.kubeconfig, 'PersistentVolumeClaim', input.snapshotId, input.namespace);
        if (!sourcePvc) {
            throw new Error(`Snapshot PVC ${input.namespace}/${input.snapshotId} not found`);
        }
        const sourceNode = sourcePvc?.metadata?.annotations?.['volume.kubernetes.io/selected-node'];
        const newPvcLabels = {
            ...input.labels,
            'flui.cloud/restored-from': input.snapshotId,
        };
        const pvcManifest = this.renderPvcManifest({
            name: input.newPvcName,
            namespace: input.namespace,
            storageClassName: input.storageClassName,
            storage: `${input.sizeGb}Gi`,
            labels: newPvcLabels,
        });
        await this.k8s.applyManifest(input.kubeconfig, pvcManifest);
        const jobName = `${input.newPvcName}-restore`;
        const placementNode = input.preferredNode ?? sourceNode;
        const jobManifest = this.renderCopyJobManifest({
            jobName,
            namespace: input.namespace,
            sourcePvcName: input.snapshotId,
            destPvcName: input.newPvcName,
            nodeSelectorHostname: placementNode,
            labels: newPvcLabels,
        });
        await this.k8s.applyManifest(input.kubeconfig, jobManifest);
        await this.waitForJobCompletion(input.kubeconfig, input.namespace, jobName, COPY_JOB_TIMEOUT_SECONDS);
        await this.cleanupCopyJob(input.kubeconfig, input.namespace, jobName);
        return { pvcName: input.newPvcName };
    }
    async cleanupCopyJob(kubeconfig, namespace, jobName) {
        // Delete child pods first — they hold the PVC `Used By` reference and
        // would block subsequent PVC deletion until kubelet finalizers tick.
        try {
            const pods = await this.k8s.listResourcesByLabel(kubeconfig, 'Pod', namespace, `job-name=${jobName}`);
            for (const pod of pods) {
                const podName = pod?.metadata?.name;
                if (!podName)
                    continue;
                await this.k8s
                    .deleteResource(kubeconfig, 'Pod', podName, namespace)
                    .catch((err) => this.logger.warn(`[hetzner-snapshot] Pod cleanup failed for ${namespace}/${podName}: ${err.message}`));
            }
        }
        catch (err) {
            this.logger.warn(`[hetzner-snapshot] Pod listing for cleanup failed in ${namespace}/${jobName}: ${err.message}`);
        }
        await this.k8s
            .deleteResource(kubeconfig, 'Job', jobName, namespace)
            .catch((err) => this.logger.warn(`[hetzner-snapshot] Job cleanup failed for ${namespace}/${jobName}: ${err.message}`));
    }
    async waitForJobCompletion(kubeconfig, namespace, jobName, timeoutSeconds) {
        const pollIntervalMs = 5000;
        const start = Date.now();
        while (Date.now() - start < timeoutSeconds * 1000) {
            const job = await this.k8s.getResource(kubeconfig, 'Job', jobName, namespace);
            const succeeded = job?.status?.succeeded ?? 0;
            const failed = job?.status?.failed ?? 0;
            if (succeeded > 0)
                return;
            if (failed > 0) {
                throw new Error(`Copy-pod Job ${namespace}/${jobName} failed (failed=${failed})`);
            }
            await new Promise((r) => setTimeout(r, pollIntervalMs));
        }
        throw new Error(`Copy-pod Job ${namespace}/${jobName} timed out after ${timeoutSeconds}s`);
    }
    renderPvcManifest(args) {
        const labelLines = Object.entries(args.labels)
            .map(([k, v]) => `    ${this.yamlString(k)}: ${this.yamlString(v)}`)
            .join('\n');
        return [
            'apiVersion: v1',
            'kind: PersistentVolumeClaim',
            'metadata:',
            `  name: ${args.name}`,
            `  namespace: ${args.namespace}`,
            '  labels:',
            labelLines,
            'spec:',
            '  accessModes:',
            '    - ReadWriteOnce',
            `  storageClassName: ${args.storageClassName}`,
            '  resources:',
            '    requests:',
            `      storage: ${args.storage}`,
            '',
        ].join('\n');
    }
    renderCopyJobManifest(args) {
        const labelLines = Object.entries(args.labels)
            .map(([k, v]) => `    ${this.yamlString(k)}: ${this.yamlString(v)}`)
            .join('\n');
        const nodeSelectorBlock = args.nodeSelectorHostname
            ? [
                '      nodeSelector:',
                `        kubernetes.io/hostname: ${args.nodeSelectorHostname}`,
            ].join('\n')
            : '';
        return [
            'apiVersion: batch/v1',
            'kind: Job',
            'metadata:',
            `  name: ${args.jobName}`,
            `  namespace: ${args.namespace}`,
            '  labels:',
            labelLines,
            'spec:',
            '  backoffLimit: 1',
            `  activeDeadlineSeconds: ${COPY_JOB_TIMEOUT_SECONDS}`,
            '  ttlSecondsAfterFinished: 30',
            '  template:',
            '    metadata:',
            '      labels:',
            labelLines,
            '    spec:',
            '      restartPolicy: Never',
            ...(nodeSelectorBlock ? [nodeSelectorBlock] : []),
            '      containers:',
            '        - name: copy',
            `          image: ${COPY_POD_IMAGE}`,
            '          command:',
            '            - /bin/sh',
            '            - -c',
            "            - 'set -e; cd /src && tar -cf - . | tar -C /dst -xf - && sync'",
            '          volumeMounts:',
            '            - name: src',
            '              mountPath: /src',
            '              readOnly: true',
            '            - name: dst',
            '              mountPath: /dst',
            '      volumes:',
            '        - name: src',
            '          persistentVolumeClaim:',
            `            claimName: ${args.sourcePvcName}`,
            '            readOnly: true',
            '        - name: dst',
            '          persistentVolumeClaim:',
            `            claimName: ${args.destPvcName}`,
            '',
        ].join('\n');
    }
    parseStorageGb(value) {
        if (!value)
            return 0;
        const match = String(value).match(/^(\d+(?:\.\d+)?)([KMGTP]i?)?$/);
        if (!match)
            return 0;
        const num = Number.parseFloat(match[1]);
        const unit = match[2];
        switch (unit) {
            case 'Ki':
                return num / (1024 * 1024);
            case 'Mi':
                return num / 1024;
            case 'Gi':
                return num;
            case 'Ti':
                return num * 1024;
            case 'K':
                return num / 1_000_000;
            case 'M':
                return num / 1000;
            case 'G':
                return num;
            case 'T':
                return num * 1000;
            default:
                return num;
        }
    }
    yamlString(value) {
        return JSON.stringify(value);
    }
};
exports.HetznerVolumeOperationsService = HetznerVolumeOperationsService;
exports.HetznerVolumeOperationsService = HetznerVolumeOperationsService = HetznerVolumeOperationsService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [kubernetes_service_1.KubernetesService])
], HetznerVolumeOperationsService);
