// retoor <retoor@molodetz.nl>

const crypto = require("crypto");
const fs = require("fs");
const http = require("http");
const https = require("https");
const path = require("path");
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 PRESET_KEY = "devplace.panelPreset";
const WELCOME_KEY = "devplace.welcomeShown";
const PANEL_STEPS = { short: 2, normal: 4, tall: 7, maximized: 0 };
const PANEL_FLOOR_STEPS = 24;
const REVEAL_TIMEOUT_MS = 15000;
const PUBLISH_TIMEOUT_MS = 20000;
const DPC_SITE_URL = "https://dpc.app.molodetz.nl/";
const DPC_DOCS_URL = "https://dpc.app.molodetz.nl/docs";
const MEMORY_FILE = "devplace-extension.json";
const WELCOME_VIEW_TYPE = "devplace.welcome";
const WELCOME_TITLE = "Welcome to DevPlace";
const WELCOME_COMMANDS = {
    agent: "devplace.runAgent",
    terminal: "workbench.action.terminal.focus",
    tunnels: "devplace.showTunnels",
    settings: "devplace.openWorkspacePage",
    project: "devplace.openProject",
    docs: "devplace.openDocs",
    walkthrough: "devplace.openWalkthrough",
};

class Profile {
    constructor() {
        this.data = Object.assign(
            {
                theme: "devplace-dark",
                layout: "standard",
                panel_preset: "normal",
                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 || "normal";
    }

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

class Memory {
    constructor(fallback) {
        this.fallback = fallback;
        const directory = process.env.DEVPLACE_WORKSPACE_STATE_DIR || "";
        this.path = directory ? path.join(directory, MEMORY_FILE) : "";
    }

    read() {
        if (!this.path) return null;
        try {
            const parsed = JSON.parse(fs.readFileSync(this.path, "utf8"));
            return parsed && typeof parsed === "object" ? parsed : {};
        } catch (error) {
            return error.code === "ENOENT" ? {} : null;
        }
    }

    get(key) {
        const stored = this.read();
        if (stored === null) return this.fallback.get(key);
        return stored[key];
    }

    async update(key, value) {
        const stored = this.read();
        if (stored === null) {
            await this.fallback.update(key, value);
            return;
        }
        stored[key] = value;
        fs.writeFileSync(this.path, JSON.stringify(stored, null, 2));
    }
}

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()) {
            await this.reveal();
            return null;
        }
        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;
        const focused = agent || shell;
        if (focused) focused.show(true);
        return focused;
    }

    async reveal() {
        const existing = vscode.window.terminals;
        if (existing.length) {
            existing[existing.length - 1].show(true);
            return;
        }
        await new Promise((resolve) => {
            const timer = setTimeout(() => {
                listener.dispose();
                resolve();
            }, REVEAL_TIMEOUT_MS);
            const listener = vscode.window.onDidOpenTerminal((terminal) => {
                clearTimeout(timer);
                listener.dispose();
                terminal.show(true);
                resolve();
            });
        });
    }

    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, memento) {
        this.profile = profile;
        this.memento = memento;
    }

    alreadyApplied() {
        return this.memento.get(PRESET_KEY) === this.profile.panelPreset;
    }

    async apply(terminal) {
        if (!terminal || this.alreadyApplied()) return false;
        const preset = this.profile.panelPreset;
        terminal.show(false);
        if (preset === "maximized") {
            await vscode.commands.executeCommand("workbench.action.toggleMaximizedPanel");
        } else {
            const steps = PANEL_STEPS[preset] === undefined ? PANEL_STEPS.normal : PANEL_STEPS[preset];
            await this.resize("workbench.action.decreaseViewSize", PANEL_FLOOR_STEPS);
            await this.resize("workbench.action.increaseViewSize", steps);
        }
        await this.memento.update(PRESET_KEY, preset);
        return true;
    }

    async resize(command, steps) {
        for (let index = 0; index < steps; index += 1) {
            await vscode.commands.executeCommand(command);
        }
    }
}

class Welcome {
    constructor(context, profile, output, memory) {
        this.context = context;
        this.profile = profile;
        this.output = output;
        this.memory = memory;
        this.panel = null;
        this.mediaUri = vscode.Uri.joinPath(context.extensionUri, "media");
    }

    alreadyShown() {
        return this.memory.get(WELCOME_KEY) === true;
    }

    register() {
        this.context.subscriptions.push(
            vscode.window.registerWebviewPanelSerializer(WELCOME_VIEW_TYPE, {
                deserializeWebviewPanel: (panel) => this.adopt(panel),
            }),
            vscode.commands.registerCommand("devplace.showWelcome", () => this.open(false)),
            vscode.commands.registerCommand("devplace.openWalkthrough", () =>
                vscode.commands.executeCommand(
                    "workbench.action.openWalkthrough",
                    "devplace.devplace-workspace#devplace.getStarted",
                    false,
                ),
            ),
        );
    }

    async openOnBoot() {
        if (this.alreadyShown()) return false;
        await this.memory.update(WELCOME_KEY, true);
        this.open(true);
        return true;
    }

    open(preserveFocus) {
        if (this.panel) {
            this.panel.reveal(undefined, preserveFocus);
            return;
        }
        this.adopt(
            vscode.window.createWebviewPanel(
                WELCOME_VIEW_TYPE,
                WELCOME_TITLE,
                { viewColumn: vscode.ViewColumn.One, preserveFocus: Boolean(preserveFocus) },
                { enableScripts: true, localResourceRoots: [this.mediaUri] },
            ),
        );
    }

    adopt(panel) {
        this.panel = panel;
        panel.iconPath = vscode.Uri.joinPath(this.mediaUri, "devplace-icon.png");
        panel.webview.options = { enableScripts: true, localResourceRoots: [this.mediaUri] };
        panel.webview.html = this.html(panel.webview);
        panel.webview.onDidReceiveMessage(
            (message) => this.onMessage(message),
            null,
            this.context.subscriptions,
        );
        panel.onDidDispose(
            () => {
                if (this.panel === panel) this.panel = null;
            },
            null,
            this.context.subscriptions,
        );
    }

    onMessage(message) {
        const command = WELCOME_COMMANDS[message && message.action];
        if (!command) return;
        vscode.commands.executeCommand(command).then(undefined, (error) =>
            this.output.appendLine(`welcome: ${command} failed: ${error}`),
        );
    }

    html(webview) {
        const template = fs.readFileSync(
            path.join(this.mediaUri.fsPath, "welcome.html"),
            "utf8",
        );
        const values = {
            cspSource: webview.cspSource,
            nonce: crypto.randomBytes(16).toString("hex"),
            styleUri: webview.asWebviewUri(vscode.Uri.joinPath(this.mediaUri, "welcome.css")).toString(),
            iconUri: webview.asWebviewUri(vscode.Uri.joinPath(this.mediaUri, "devplace-icon.png")).toString(),
            projectTitle: this.profile.projectTitle,
            dpcSiteUrl: DPC_SITE_URL,
            dpcDocsUrl: DPC_DOCS_URL,
            agentStarted: this.profile.wantsAgent ? "already running" : "one command away",
        };
        return template.replace(/\{\{(\w+)\}\}/g, (match, key) =>
            key in values ? this.escape(values[key]) : match,
        );
    }

    escape(value) {
        return String(value)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;");
    }
}

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();

    const memory = new Memory(context.globalState);
    const welcome = new Welcome(context, profile, output, memory);

    await stage(output, "presence", () => new Presence(context).register());
    await stage(output, "welcome-commands", () => welcome.register());
    const focused = await stage(output, "terminals", () =>
        new BootTerminals(profile, memory).open(),
    );
    await stage(output, "welcome", () => welcome.openOnBoot());
    await stage(output, "layout", () =>
        new Layout(profile, memory).apply(focused || null),
    );
    await stage(output, "tunnels", () => new Tunnels(output).watch(context));
}

function deactivate() {}

module.exports = { activate, deactivate };
