/**
 * Screen capture backend for macOS.
 * Uses `screencapture` CLI for screenshots and screen recording.
 * Captures are returned as base64-encoded images for vision model consumption.
 */
import { execSync, spawn } from "child_process";
import { EventEmitter } from "events";
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
export class ScreenCapture extends EventEmitter {
    get recording() {
        return this._recording;
    }
    constructor() {
        super();
        this.recordProcess = null;
        this._recording = false;
        this.recordingPath = null;
        this.captureDir = join(tmpdir(), "avis-vision");
        if (!existsSync(this.captureDir)) {
            mkdirSync(this.captureDir, { recursive: true });
        }
    }
    /**
     * List available displays.
     */
    listDisplays() {
        try {
            const output = execSync(`system_profiler SPDisplaysDataType 2>/dev/null`, {
                encoding: "utf-8",
                timeout: 5000,
            });
            const displays = [];
            const blocks = output.split(/(?=Display Type:|Online:)/);
            let id = 1;
            for (const block of blocks) {
                const resMatch = block.match(/Resolution:\s*(\d+\s*x\s*\d+)/);
                const mainMatch = block.match(/Main Display:\s*Yes/i);
                const typeMatch = block.match(/Display Type:\s*(.+)/);
                if (resMatch) {
                    displays.push({
                        id,
                        name: typeMatch?.[1]?.trim() ?? `Display ${id}`,
                        resolution: resMatch[1].trim(),
                        isMain: !!mainMatch,
                    });
                    id++;
                }
            }
            return displays;
        }
        catch {
            return [{ id: 1, name: "Main Display", resolution: "unknown", isMain: true }];
        }
    }
    /**
     * Take a screenshot of a specific display (or main display by default).
     * Returns the file path of the captured PNG.
     */
    async screenshot(displayId) {
        const fileName = `avis-screen-${Date.now()}.png`;
        const filePath = join(this.captureDir, fileName);
        const args = ["-x", "-t", "png"]; // silent, PNG format
        if (displayId) {
            args.push("-D", String(displayId));
        }
        args.push(filePath);
        return new Promise((resolve, reject) => {
            const proc = spawn("screencapture", args, { stdio: "pipe" });
            proc.on("close", (code) => {
                if (code === 0 && existsSync(filePath)) {
                    this.emit("captured", filePath);
                    resolve(filePath);
                }
                else {
                    const msg = `screencapture failed with code ${code}`;
                    this.emit("error", msg);
                    reject(new Error(msg));
                }
            });
            proc.on("error", (err) => {
                this.emit("error", err.message);
                reject(err);
            });
        });
    }
    /**
     * Start recording the screen. Call stopRecording() to finish.
     */
    startRecording(displayId) {
        if (this._recording)
            return;
        const fileName = `avis-record-${Date.now()}.mov`;
        this.recordingPath = join(this.captureDir, fileName);
        const args = ["-v", "-x"]; // video mode, silent
        if (displayId) {
            args.push("-D", String(displayId));
        }
        args.push(this.recordingPath);
        this.recordProcess = spawn("screencapture", args, { stdio: "pipe" });
        this._recording = true;
        this.emit("started", "record");
        this.recordProcess.on("close", () => {
            this._recording = false;
            this.recordProcess = null;
        });
        this.recordProcess.on("error", (err) => {
            this._recording = false;
            this.recordProcess = null;
            this.emit("error", err.message);
        });
    }
    /**
     * Stop recording and extract key frames as images.
     * Returns paths to extracted frame images.
     */
    async stopRecording() {
        if (!this.recordProcess || !this.recordingPath) {
            this.emit("stopped");
            return [];
        }
        const videoPath = this.recordingPath;
        // Send SIGINT to gracefully stop screencapture recording
        this.recordProcess.kill("SIGINT");
        // Wait for process to close
        await new Promise((resolve) => {
            if (!this.recordProcess) {
                resolve();
                return;
            }
            this.recordProcess.on("close", () => resolve());
            // Fallback timeout
            setTimeout(() => {
                if (this.recordProcess) {
                    this.recordProcess.kill("SIGKILL");
                }
                resolve();
            }, 3000);
        });
        this._recording = false;
        this.recordProcess = null;
        this.recordingPath = null;
        if (!existsSync(videoPath)) {
            this.emit("error", "Recording file not found");
            this.emit("stopped");
            return [];
        }
        // Extract frames from the video
        const frames = await this.extractFrames(videoPath);
        this.emit("frames_ready", frames);
        this.emit("stopped");
        // Clean up video file
        try {
            unlinkSync(videoPath);
        }
        catch {
            // ignore
        }
        return frames;
    }
    /**
     * Extract key frames from a video file using sips + qlmanage (macOS native).
     * Falls back to taking screenshots of first/last frame if tools not available.
     */
    async extractFrames(videoPath) {
        const framesDir = join(this.captureDir, `frames-${Date.now()}`);
        mkdirSync(framesDir, { recursive: true });
        try {
            // Try using ffmpeg if available
            execSync("which ffmpeg", { stdio: "pipe" });
            // Extract up to 5 frames evenly spaced
            const duration = this.getVideoDuration(videoPath);
            const frameCount = Math.min(5, Math.max(1, Math.floor(duration)));
            const interval = duration / (frameCount + 1);
            for (let i = 1; i <= frameCount; i++) {
                const time = (interval * i).toFixed(2);
                const framePath = join(framesDir, `frame-${i}.png`);
                try {
                    execSync(`ffmpeg -ss ${time} -i "${videoPath}" -vframes 1 -f image2 "${framePath}" -y 2>/dev/null`, {
                        timeout: 10000,
                    });
                }
                catch {
                    // skip this frame
                }
            }
        }
        catch {
            // No ffmpeg -- use qlmanage to generate a thumbnail of the video
            const thumbPath = join(framesDir, "frame-1.png");
            try {
                execSync(`qlmanage -t -s 1920 -o "${framesDir}" "${videoPath}" 2>/dev/null`, { timeout: 10000 });
                // qlmanage outputs as videoname.mov.png
                const files = readdirSync(framesDir);
                if (files.length > 0 && files[0] !== "frame-1.png") {
                    const srcPath = join(framesDir, files[0]);
                    if (existsSync(srcPath)) {
                        const { renameSync } = require("fs");
                        renameSync(srcPath, thumbPath);
                    }
                }
            }
            catch {
                // Last resort: take a fresh screenshot as a "frame"
                try {
                    const screenshotPath = join(framesDir, "frame-1.png");
                    execSync(`screencapture -x -t png "${screenshotPath}"`, { timeout: 5000 });
                }
                catch {
                    // nothing we can do
                }
            }
        }
        // Collect all frame files
        const frames = [];
        if (existsSync(framesDir)) {
            for (const f of readdirSync(framesDir).sort()) {
                const fp = join(framesDir, f);
                if (f.endsWith(".png") || f.endsWith(".jpg")) {
                    frames.push(fp);
                }
            }
        }
        return frames;
    }
    getVideoDuration(videoPath) {
        try {
            const output = execSync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}" 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
            return parseFloat(output.trim()) || 5;
        }
        catch {
            return 5; // default assumption
        }
    }
    /**
     * Read an image file and return it as a base64 ImageContent object.
     */
    static imageToBase64(filePath) {
        const buffer = readFileSync(filePath);
        const ext = filePath.split(".").pop()?.toLowerCase();
        const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
        return {
            data: buffer.toString("base64"),
            mimeType,
        };
    }
    /**
     * Clean up temporary files.
     */
    cleanup() {
        if (this.recordProcess) {
            this.recordProcess.kill("SIGKILL");
            this.recordProcess = null;
        }
        this._recording = false;
        // Clean up capture dir files older than 1 hour
        try {
            const now = Date.now();
            for (const f of readdirSync(this.captureDir)) {
                const fp = join(this.captureDir, f);
                try {
                    const { statSync } = require("fs");
                    const stat = statSync(fp);
                    if (now - stat.mtimeMs > 3600000) {
                        unlinkSync(fp);
                    }
                }
                catch {
                    // ignore
                }
            }
        }
        catch {
            // ignore
        }
    }
}
//# sourceMappingURL=screen-capture.js.map