import crypto from "crypto";
import { z } from "zod";
import { apiClient, stripSensitiveData, withProviderErrorHandling, assertValidServerId, uploadSshKeyWithConflict } from "./base.js";
import { BusinessError } from "../utils/errors.js";
import { withRetry } from "../utils/retry.js";
import { formatSnapshotCost } from "../constants.js";
import { debugLog } from "../utils/logger.js";
export const LinodeInstanceSchema = z.object({
    id: z.number(),
    status: z.string(),
    ipv4: z.array(z.string()).optional(),
});
function extractLinodeError(data) {
    const d = data;
    if (!Array.isArray(d?.errors))
        return undefined;
    const reasons = d.errors.map((e) => e.reason).join(", ");
    return reasons || undefined;
}
export class LinodeProvider {
    name = "linode";
    displayName = "Linode (Akamai)";
    apiToken;
    baseUrl = "https://api.linode.com/v4";
    constructor(apiToken) {
        this.apiToken = apiToken;
    }
    async validateToken(token) {
        try {
            await withRetry(async () => {
                await apiClient.get(`${this.baseUrl}/profile`, {
                    headers: { Authorization: `Bearer ${token}` },
                });
            });
            return true;
        }
        catch {
            return false;
        }
    }
    async uploadSshKey(name, publicKey) {
        return uploadSshKeyWithConflict(name, publicKey, {
            apiToken: this.apiToken,
            baseUrl: this.baseUrl,
            createPath: "/profile/sshkeys",
            bodyKeyField: "ssh_key",
            nameField: "label",
            listPath: "/profile/sshkeys",
            listArrayField: "data",
            listKeyField: "ssh_key",
            conflictStatuses: [400],
        });
    }
    async createServer(config) {
        return withProviderErrorHandling("create server", async () => {
            // Ensure all character classes for Linode password strength: upper + lower + digit + special
            const rootPass = `Ql1!${crypto.randomBytes(21).toString("base64").slice(0, 28)}`;
            const body = {
                label: config.name,
                type: config.size,
                region: config.region,
                image: "linode/ubuntu24.04",
                root_pass: rootPass,
                metadata: { user_data: Buffer.from(config.cloudInit).toString("base64") },
            };
            if (config.sshKeyIds?.length) {
                // Linode uses authorized_users (profile usernames) to inject SSH keys.
                // Fetch profile username; if it fails, cloud-init handles SSH key injection as fallback.
                try {
                    const profileRes = await apiClient.get(`${this.baseUrl}/profile`, {
                        headers: { Authorization: `Bearer ${this.apiToken}` },
                    });
                    const username = profileRes.data?.username;
                    if (typeof username === "string" && username.length > 0) {
                        body.authorized_users = [username];
                    }
                }
                catch {
                    // Profile fetch failed; cloud-init will handle SSH key setup
                }
            }
            const response = await apiClient.post(`${this.baseUrl}/linode/instances`, body, {
                headers: {
                    Authorization: `Bearer ${this.apiToken}`,
                    "Content-Type": "application/json",
                },
            });
            const instance = LinodeInstanceSchema.parse(response.data);
            return {
                id: instance.id.toString(),
                ip: instance.ipv4?.[0] || "pending",
                status: instance.status === "provisioning" ? "initializing" : instance.status,
            };
        }, extractLinodeError);
    }
    catch(error) {
        debugLog?.("linode profile fetch failed", { cause: error });
    }
    async getServerDetails(serverId) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("get server details", () => withRetry(async () => {
            const response = await apiClient.get(`${this.baseUrl}/linode/instances/${serverId}`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
            const instance = LinodeInstanceSchema.parse(response.data);
            return {
                id: instance.id.toString(),
                ip: instance.ipv4?.[0] || "pending",
                status: instance.status === "running" ? "running" : instance.status,
            };
        }));
    }
    async getServerStatus(serverId) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("get server status", () => withRetry(async () => {
            const response = await apiClient.get(`${this.baseUrl}/linode/instances/${serverId}`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
            const instance = LinodeInstanceSchema.parse(response.data);
            return instance.status;
        }));
    }
    async destroyServer(serverId) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("destroy server", async () => {
            await apiClient.delete(`${this.baseUrl}/linode/instances/${serverId}`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
        }, extractLinodeError);
    }
    async rebootServer(serverId) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("reboot server", async () => {
            await apiClient.post(`${this.baseUrl}/linode/instances/${serverId}/reboot`, {}, {
                headers: {
                    Authorization: `Bearer ${this.apiToken}`,
                    "Content-Type": "application/json",
                },
            });
        }, extractLinodeError);
    }
    getRegions() {
        return [
            { id: "us-east", name: "Newark, NJ", location: "USA" },
            { id: "eu-west", name: "London", location: "UK" },
            { id: "eu-central", name: "Frankfurt", location: "Germany" },
            { id: "ap-south", name: "Singapore", location: "Singapore" },
        ];
    }
    getServerSizes() {
        return [
            { id: "g6-standard-2", name: "Linode 4GB", vcpu: 2, ram: 4, disk: 80, price: "$12/mo" },
            { id: "g6-standard-4", name: "Linode 8GB", vcpu: 4, ram: 8, disk: 160, price: "$24/mo" },
            { id: "g6-standard-6", name: "Linode 16GB", vcpu: 6, ram: 16, disk: 320, price: "$48/mo" },
        ];
    }
    async getAvailableLocations() {
        try {
            return await withRetry(async () => {
                const response = await apiClient.get(`${this.baseUrl}/regions`, {
                    headers: { Authorization: `Bearer ${this.apiToken}` },
                });
                return response.data.data
                    .filter((r) => r.status === "ok" && r.capabilities.includes("Linodes"))
                    .map((r) => ({
                    id: r.id,
                    name: r.label,
                    location: r.country,
                }));
            });
        }
        catch (error) {
            stripSensitiveData(error);
            return this.getRegions();
        }
    }
    async getAvailableServerTypes(_location, mode) {
        try {
            return await withRetry(async () => {
                const response = await apiClient.get(`${this.baseUrl}/linode/types`, {
                    headers: { Authorization: `Bearer ${this.apiToken}` },
                });
                const MIN_RAM_MB = mode === "bare" ? 0 : 4096; // Coolify requires at least 2GB, recommend 4GB. Bare has no minimum
                const types = response.data.data.filter((t) => t.memory >= MIN_RAM_MB && t.id.startsWith("g6-standard"));
                if (types.length === 0) {
                    return this.getServerSizes();
                }
                return types.map((t) => ({
                    id: t.id,
                    name: t.label,
                    vcpu: t.vcpus,
                    ram: Math.round(t.memory / 1024),
                    disk: Math.round(t.disk / 1024),
                    price: `$${t.price.monthly.toFixed(2)}/mo`,
                }));
            });
        }
        catch (error) {
            stripSensitiveData(error);
            return this.getServerSizes();
        }
    }
    async createSnapshot(serverId, name) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("create snapshot", async () => {
            // Get the first disk to create image from
            const disksResponse = await apiClient.get(`${this.baseUrl}/linode/instances/${serverId}/disks`, { headers: { Authorization: `Bearer ${this.apiToken}` } });
            const disks = disksResponse.data.data;
            if (!disks || disks.length === 0) {
                throw new BusinessError("No disks found on this instance", { hint: "Instance may still be provisioning — try again shortly" });
            }
            // Use the largest disk
            const disk = disks.sort((a, b) => b.size - a.size)[0];
            const response = await apiClient.post(`${this.baseUrl}/images`, { disk_id: disk.id, label: name }, {
                headers: {
                    Authorization: `Bearer ${this.apiToken}`,
                    "Content-Type": "application/json",
                },
            });
            const image = response.data;
            return {
                id: image.id,
                serverId,
                name: image.label || name,
                status: image.status,
                sizeGb: image.size ? image.size / 1024 : 0,
                createdAt: image.created,
                costPerMonth: formatSnapshotCost("linode", image.size ? image.size / 1024 : 0),
            };
        }, extractLinodeError);
    }
    async listSnapshots(serverId) {
        return withProviderErrorHandling("list snapshots", () => withRetry(async () => {
            const response = await apiClient.get(`${this.baseUrl}/images?page=1&page_size=100`, { headers: { Authorization: `Bearer ${this.apiToken}` } });
            const images = response.data.data.filter((img) => img.type === "manual" && img.label && (img.label.startsWith("kastell-") || img.label.startsWith("quicklify-")));
            return images.map((img) => ({
                id: img.id,
                serverId,
                name: img.label || "",
                status: img.status,
                sizeGb: img.size ? img.size / 1024 : 0,
                createdAt: img.created,
                costPerMonth: formatSnapshotCost("linode", img.size ? img.size / 1024 : 0),
            }));
        }), extractLinodeError);
    }
    async deleteSnapshot(snapshotId) {
        assertValidServerId(snapshotId);
        return withProviderErrorHandling("delete snapshot", async () => {
            await apiClient.delete(`${this.baseUrl}/images/${snapshotId}`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
        }, extractLinodeError);
    }
    async restoreSnapshot(serverId, snapshotId) {
        assertValidServerId(serverId);
        assertValidServerId(snapshotId);
        return withProviderErrorHandling("restore snapshot", async () => {
            await apiClient.post(`${this.baseUrl}/linode/instances/${serverId}/rebuild`, { image: snapshotId, root_pass: crypto.randomUUID() }, { headers: { Authorization: `Bearer ${this.apiToken}`, "Content-Type": "application/json" } });
        }, extractLinodeError);
    }
    async getSnapshotCostEstimate(serverId) {
        assertValidServerId(serverId);
        return withProviderErrorHandling("get snapshot cost", () => withRetry(async () => {
            const response = await apiClient.get(`${this.baseUrl}/linode/instances/${serverId}`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
            const diskMb = response.data.specs?.disk || response.data.disk || 0;
            const diskGb = diskMb / 1024;
            return `$${(diskGb * 0.004).toFixed(2)}/mo`;
        }), extractLinodeError);
    }
    async findServerByIp(ip) {
        return withProviderErrorHandling("find server by IP", async () => {
            const response = await apiClient.get(`${this.baseUrl}/linode/instances?page_size=100`, {
                headers: { Authorization: `Bearer ${this.apiToken}` },
            });
            const instances = response.data.data;
            const found = instances.find((instance) => instance.ipv4?.includes(ip));
            return found ? found.id.toString() : null;
        }, extractLinodeError);
    }
}
//# sourceMappingURL=linode.js.map