// retoor <retoor@molodetz.nl>

const fs = require("fs");
const http = require("http");
const https = require("https");
const vscode = require("vscode");

const AGENT_PATH = "/usr/bin/dpc";
const AGENT_TERMINAL = "DevPlace Code";
const SHELL_TERMINAL = "pravda@workspace";
const BOOT_KEY = "devplace.bootMarker";
const PANEL_STEPS = { short: 0, normal: 2, tall: 5, maximized: 0 };
const PUBLISH_TIMEOUT_MS = 20000;

class Profile {
    constructor() {
        this.data = Object.assign(
            {
                theme: "devplace-dark",
                layout: "standard",
                panel_preset: "tall",
                boot_agent: "dpc",
                boot_shell: true,
                trust_all: true,
            },
            this.fromFile(),
            this.fromEnv(),
        );
    }

    fromFile() {
        const path = process.env.DEVPLACE_EDITOR_PROFILE;
        if (!path) return {};
        try {
            const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
            return parsed && parsed.editor ? parsed.editor : {};
        } catch (error) {
            return {};
        }
    }

    fromEnv() {
        const values = {};
        if (process.env.DEVPLACE_EDITOR_PANEL_PRESET) {
            values.panel_preset = process.env.DEVPLACE_EDITOR_PANEL_PRESET;
        }
        if (process.env.DEVPLACE_EDITOR_BOOT_AGENT) {
            values.boot_agent = process.env.DEVPLACE_EDITOR_BOOT_AGENT;
        }
        if (process.env.DEVPLACE_EDITOR_BOOT_SHELL) {
            values.boot_shell = process.env.DEVPLACE_EDITOR_BOOT_SHELL === "1";
        }
        return values;
    }

    get bootMarker() {
        return process.env.DEVPLACE_CONTAINER_BOOT || `host-${process.pid}`;
    }

    get wantsAgent() {
        return this.data.boot_agent === "dpc" && fs.existsSync(AGENT_PATH);
    }

    get wantsShell() {
        return Boolean(this.data.boot_shell);
    }

    get panelPreset() {
        return this.data.panel_preset || "tall";
    }
}

class BootTerminals {
    constructor(profile, memento) {
        this.profile = profile;
        this.memento = memento;
    }

    alreadyBooted() {
        return this.memento.get(BOOT_KEY) === this.profile.bootMarker;
    }

    async open() {
        if (this.alreadyBooted()) return false;
        await this.memento.update(BOOT_KEY, this.profile.bootMarker);
        const shell = this.profile.wantsShell ? this.createShell() : null;
        const agent = this.profile.wantsAgent ? this.createAgent() : null;
        if (agent) agent.show(true);
        else if (shell) shell.show(true);
        return Boolean(agent || shell);
    }

    createAgent() {
        return vscode.window.createTerminal({
            name: AGENT_TERMINAL,
            shellPath: "/bin/bash",
            shellArgs: ["-l", "-c", `exec ${AGENT_PATH}`],
            iconPath: new vscode.ThemeIcon("rocket"),
            isTransient: false,
        });
    }

    createShell() {
        return vscode.window.createTerminal({
            name: SHELL_TERMINAL,
            shellPath: "/bin/bash",
            shellArgs: ["-l"],
            iconPath: new vscode.ThemeIcon("terminal-bash"),
            isTransient: false,
        });
    }
}

class Layout {
    constructor(profile) {
        this.profile = profile;
    }

    async apply(panelIsOpen) {
        const preset = this.profile.panelPreset;
        if (!panelIsOpen) return;
        if (preset === "maximized") {
            await vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel");
            return;
        }
        const steps = PANEL_STEPS[preset] === undefined ? 5 : PANEL_STEPS[preset];
        for (let index = 0; index < steps; index += 1) {
            await vscode.commands.executeCommand("workbench.action.increaseViewSize");
        }
    }
}

class Presence {
    constructor(context) {
        this.context = context;
        this.item = vscode.window.createStatusBarItem(
            vscode.StatusBarAlignment.Left,
            100,
        );
    }

    get projectTitle() {
        return process.env.DEVPLACE_PROJECT_TITLE || "DevPlace";
    }

    register() {
        this.item.text = `$(rocket) ${this.projectTitle}`;
        this.item.tooltip = this.tooltip();
        this.item.command = "devplace.openProject";
        this.item.show();
        this.context.subscriptions.push(this.item);
        this.registerCommands();
    }

    tooltip() {
        const owner = process.env.DEVPLACE_WORKSPACE_OWNER || "";
        const name = process.env.DEVPLACE_TUNNEL_NAME || "";
        return `DevPlace workspace ${name}${owner ? ` for ${owner}` : ""}`;
    }

    registerCommands() {
        const commands = {
            "devplace.runAgent": () => this.runAgent(),
            "devplace.openProject": () => this.open(process.env.DEVPLACE_PROJECT_URL),
            "devplace.openWorkspacePage": () =>
                this.open(process.env.DEVPLACE_WORKSPACE_URL),
            "devplace.openDocs": () => this.openDocs(),
            "devplace.showTunnels": () => this.showTunnels(),
        };
        for (const [name, handler] of Object.entries(commands)) {
            this.context.subscriptions.push(
                vscode.commands.registerCommand(name, handler),
            );
        }
    }

    runAgent() {
        const terminal = vscode.window.createTerminal({
            name: AGENT_TERMINAL,
            shellPath: AGENT_PATH,
            iconPath: new vscode.ThemeIcon("rocket"),
        });
        terminal.show(true);
    }

    openDocs() {
        const base = process.env.DEVPLACE_BASE_URL || "";
        this.open(base ? `${base}/docs/workspace-editor.html` : "");
    }

    open(url) {
        if (!url) {
            vscode.window.showWarningMessage(
                "DevPlace has not published a site URL for this workspace yet.",
            );
            return;
        }
        vscode.env.openExternal(vscode.Uri.parse(url));
    }

    async showTunnels() {
        const path = process.env.DEVPLACE_TUNNEL_MANIFEST;
        const rows = this.readManifest(path);
        if (!rows.length) {
            vscode.window.showInformationMessage(
                "This workspace has no public tunnels yet.",
            );
            return;
        }
        const picked = await vscode.window.showQuickPick(
            rows.map((row) => ({
                label: row.label || row.hostname,
                description: row.url,
                detail: `port ${row.container_port} - ${row.status}`,
                url: row.url,
            })),
            { placeHolder: "Open a public tunnel" },
        );
        if (picked) this.open(picked.url);
    }

    readManifest(path) {
        if (!path) return [];
        try {
            const parsed = JSON.parse(fs.readFileSync(path, "utf8"));
            return Array.isArray(parsed.tunnels) ? parsed.tunnels : [];
        } catch (error) {
            return [];
        }
    }
}

class Tunnels {
    constructor(output) {
        this.output = output;
        this.published = new Set();
        this.base = (process.env.DEVPLACE_BASE_URL || "").replace(/\/+$/, "");
        this.apiKey = process.env.DEVPLACE_API_KEY || "";
        this.slug = process.env.DEVPLACE_PROJECT_SLUG || "";
        this.editorPort = Number(process.env.DEVPLACE_EDITOR_PORT || 0);
    }

    get configured() {
        return Boolean(this.base && this.apiKey && this.slug);
    }

    async watch(context) {
        if (!this.configured) {
            this.output.appendLine(
                "tunnels: this workspace has no DevPlace credentials, so forwarded ports stay private",
            );
            return;
        }
        context.subscriptions.push(
            vscode.workspace.onDidChangeTunnels(() =>
                this.sync().catch((error) =>
                    this.output.appendLine(`tunnels: sync failed: ${error}`),
                ),
            ),
        );
        await this.sync();
    }

    async sync() {
        const rows = (await vscode.workspace.tunnels) || [];
        for (const row of rows) {
            const port = Number((row.remoteAddress || {}).port || 0);
            if (!port || port === this.editorPort) continue;
            if (this.published.has(port)) continue;
            await this.publish(port);
        }
    }

    async publish(port) {
        this.published.add(port);
        let answer;
        try {
            answer = await this.post(port);
        } catch (error) {
            this.published.delete(port);
            this.output.appendLine(`tunnels: port ${port} could not be published: ${error}`);
            return;
        }
        if (answer.status >= 400) {
            this.published.delete(port);
            this.output.appendLine(
                `tunnels: DevPlace refused port ${port} (${answer.status}): ${answer.body.slice(0, 300)}`,
            );
            vscode.window.showWarningMessage(
                `DevPlace could not publish port ${port}: ${this.refusal(answer.body)}`,
            );
            return;
        }
        const url = this.publishedUrl(answer.body, port);
        this.output.appendLine(`tunnels: port ${port} is published at ${url}`);
        vscode.window.showInformationMessage(
            `Port ${port} is published at ${url}. It serves HTTPS once its certificate is issued.`,
        );
    }

    post(port) {
        const url = new URL(
            `${this.base}/projects/${encodeURIComponent(this.slug)}/workspace/tunnels`,
        );
        const body = new URLSearchParams({
            label: `Port ${port}`,
            container_port: String(port),
        }).toString();
        const client = url.protocol === "https:" ? https : http;
        const options = {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Content-Length": Buffer.byteLength(body),
                Accept: "application/json",
                "X-API-KEY": this.apiKey,
            },
            timeout: PUBLISH_TIMEOUT_MS,
        };
        return new Promise((resolve, reject) => {
            const call = client.request(url, options, (response) => {
                const chunks = [];
                response.on("data", (chunk) => chunks.push(chunk));
                response.on("end", () =>
                    resolve({
                        status: response.statusCode,
                        body: Buffer.concat(chunks).toString("utf8"),
                    }),
                );
            });
            call.on("timeout", () => call.destroy(new Error("request timed out")));
            call.on("error", reject);
            call.end(body);
        });
    }

    refusal(body) {
        try {
            const parsed = JSON.parse(body);
            return (parsed.error && parsed.error.message) || "the request was refused";
        } catch (error) {
            return "the request was refused";
        }
    }

    publishedUrl(body, port) {
        try {
            const parsed = JSON.parse(body);
            const hostname = parsed.data && parsed.data.hostname;
            if (hostname) return `https://${hostname}`;
        } catch (error) {
            /* fall through to the pattern below */
        }
        const pattern =
            process.env.DEVPLACE_TUNNEL_PORT_PATTERN || "{port}-{name}.{domain}";
        return `https://${pattern
            .replace("{port}", String(port))
            .replace("{name}", process.env.DEVPLACE_TUNNEL_NAME || "")
            .replace("{domain}", process.env.DEVPLACE_TUNNEL_DOMAIN || "")}`;
    }
}

async function stage(output, name, run) {
    try {
        return await run();
    } catch (error) {
        output.appendLine(`${name} failed: ${error}`);
        return undefined;
    }
}

async function activate(context) {
    const output = vscode.window.createOutputChannel("DevPlace");
    context.subscriptions.push(output);
    const profile = new Profile();

    await stage(output, "presence", () => new Presence(context).register());
    const opened = await stage(output, "terminals", () =>
        new BootTerminals(profile, context.workspaceState).open(),
    );
    await stage(output, "layout", () => new Layout(profile).apply(Boolean(opened)));
    await stage(output, "tunnels", () => new Tunnels(output).watch(context));
}

function deactivate() {}

module.exports = { activate, deactivate };
