import pc from "picocolors";
import { WebSocket } from "ws";
import { logger } from "../util/logger.js";
export const createTail = async (api, id, scriptName) => {
    const tail = new TailClient(api, id, scriptName);
    await tail.connect();
    return tail;
};
const DEBUG = !!process.env.DEBUG;
class TailClient {
    api;
    id;
    scriptName;
    tail;
    ws;
    pingInterval;
    clean = false;
    constructor(api, id, scriptName) {
        this.api = api;
        this.id = id;
        this.scriptName = scriptName;
    }
    async create() {
        if (this.tail &&
            this.tail.expires_at > new Date(Date.now() + 1000 * 60 * 5)) {
            return this.tail;
        }
        const res = await this.api.post(`/accounts/${this.api.accountId}/workers/scripts/${this.scriptName}/tails`, { filters: [] }, {
            headers: {
                "Content-Type": "application/json",
            },
        });
        const json = (await res.json());
        if (!json.success) {
            throw new Error(`Failed to create tail for ${this.scriptName} (${res.status}): ${json.errors.map((e) => `${e.code} - ${e.message}`).join("\n")}`);
        }
        this.tail = {
            id: json.result.id,
            url: json.result.url,
            expires_at: new Date(json.result.expires_at),
        };
        return this.tail;
    }
    async connect(attempt = 0) {
        if (this.ws) {
            throw new Error(`Tail already connected for ${this.scriptName}`);
        }
        if (attempt > 3) {
            await this.close();
            throw new Error(`Failed to connect to tail for ${this.scriptName}`);
        }
        const tail = await this.create();
        const ws = new WebSocket(tail.url, "trace-v1");
        ws.on("open", () => {
            this.ping(ws);
            if (DEBUG) {
                logger.log(`connected to tail for "${this.scriptName}"`);
            }
        });
        ws.on("close", () => {
            this.ws = undefined;
            if (DEBUG) {
                logger.log(`closed tail for "${this.scriptName}"`);
            }
            if (this.clean) {
                return;
            }
            logger.log(`reconnecting to tail for "${this.scriptName}"`);
            setTimeout(() => {
                this.connect(attempt + 1);
            }, 1000);
        });
        ws.on("error", (event) => {
            logger.error(`error on tail for "${this.scriptName}": ${event}`);
        });
        ws.on("message", (message) => {
            const data = JSON.parse(message.toString());
            const prefix = pc.blue(`[${this.id}]`);
            if (data.event && "request" in data.event) {
                // TODO: handle other event types
                const event = data.event;
                const url = new URL(event.request.url);
                const status = event.response?.status ?? 500;
                // TODO: make this look nicer
                logger.log(`${prefix} ${pc.gray(event.request.method)} ${url.pathname} ${pc.dim(">")} ${status >= 200 && status < 300 ? pc.green(status) : pc.red(status)} ${pc.gray(`(cpu: ${Math.round(data.cpuTime)}ms, wall: ${Math.round(data.wallTime)}ms)`)}`);
            }
            for (const log of data.logs) {
                logger.log(`${prefix} ${pc.gray(log.level)} ${log.message.join(" ")}`);
            }
            for (const exception of data.exceptions) {
                const start = `${prefix} ${pc.red(exception.name)}`;
                logger.log(`${start} ${exception.message}\n${pc.gray(exception.stack)}`);
            }
        });
    }
    async ping(ws) {
        let waitingForPong = false;
        const cleanup = () => {
            if (!this.pingInterval) {
                return;
            }
            clearInterval(this.pingInterval);
            this.pingInterval = undefined;
        };
        this.pingInterval = setInterval(() => {
            if (ws.readyState !== WebSocket.OPEN) {
                cleanup();
                return;
            }
            if (waitingForPong) {
                ws.close();
            }
            waitingForPong = true;
            ws.ping();
        }, 10_000);
        ws.on("pong", () => {
            waitingForPong = false;
        });
    }
    async close() {
        this.clean = true;
        if (this.pingInterval) {
            clearInterval(this.pingInterval);
            this.pingInterval = undefined;
        }
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.close();
            this.ws = undefined;
        }
        if (this.tail) {
            const res = await this.api.delete(`/accounts/${this.api.accountId}/workers/scripts/${this.scriptName}/tails/${this.tail.id}`);
            if (!res.ok) {
                throw new Error(`Failed to close tail for ${this.scriptName} (${res.status}): ${res.statusText}`);
            }
            this.tail = undefined;
            if (DEBUG) {
                logger.log(`closed tail for "${this.scriptName}": ${res.status}`);
            }
        }
    }
}
//# sourceMappingURL=worker-tail.js.map