/**
 * Kimi CLI 배너 필터 — 기본 웰컴 배너를 키미캣 커스텀 배너로 교체
 *
 * node-pty 스트림에서 Kimi CLI의 기본 배너 블록을 감지하고,
 * Directory / Session / Model 메타 정보를 추출한 뒤
 * omk 테마의 커스텀 배너로 대체 출력합니다.
 */
function stripAnsi(str) {
    return str.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
}
export class BannerReplacer {
    onReplace;
    stripOnly;
    onMeta;
    state = "buffering";
    chunks = [];
    strippedCache = "";
    cacheDirty = true;
    timeout = null;
    MAX_LINES = 120; // banner may include model info / ascii art / MCP status
    TIMEOUT_MS = 3000; // allow slower terminals / ssh latency / MCP connection status
    MAX_BYTES = 32768; // accommodate larger ascii art banners
    constructor(onReplace, stripOnly = false, onMeta) {
        this.onReplace = onReplace;
        this.stripOnly = stripOnly;
        this.onMeta = onMeta;
    }
    get buffer() {
        return this.chunks.join("");
    }
    getStripped() {
        if (this.cacheDirty) {
            this.strippedCache = stripAnsi(this.buffer);
            this.cacheDirty = false;
        }
        return this.strippedCache;
    }
    /** 데이터 청크를 받아 배너 교체 또는 그대로 통과시킴 */
    process(data) {
        if (this.state === "replaced" || this.state === "stripped") {
            const stripped = this.stripKimiBanner(data);
            return stripped.length > 0 ? stripped : null;
        }
        if (this.state === "passthrough") {
            return data;
        }
        data = this.passthroughTerminalSetupPrefix(data);
        if (data.length === 0)
            return null;
        if (this.timeout === null) {
            this.startTimeout();
        }
        this.chunks.push(data);
        this.cacheDirty = true;
        const buf = this.buffer;
        if (buf.length > this.MAX_BYTES) {
            return this.flushPassthrough();
        }
        const lineCount = this.countLines(buf);
        if (lineCount > this.MAX_LINES) {
            return this.flushPassthrough();
        }
        const clean = this.getStripped();
        if (this.isBannerComplete(clean)) {
            return this.flushReplace();
        }
        if (!this.shouldContinueBuffering(clean)) {
            return this.flushPassthrough();
        }
        return null;
    }
    /** 타임아웃 등으로 인한 강제 플러시 */
    forceFlush() {
        this.clearTimeout();
        if (this.state === "buffering") {
            return this.flushPassthrough();
        }
        return null;
    }
    startTimeout() {
        this.clearTimeout();
        this.timeout = setTimeout(() => {
            if (this.state === "buffering") {
                const rest = this.flushPassthrough();
                if (rest !== null) {
                    process.stdout.write(rest);
                }
            }
        }, this.TIMEOUT_MS);
    }
    clearTimeout() {
        if (this.timeout !== null) {
            clearTimeout(this.timeout);
            this.timeout = null;
        }
    }
    countLines(buf) {
        if (buf.length === 0)
            return 0;
        return buf.split(/\r?\n/).length;
    }
    passthroughTerminalSetupPrefix(data) {
        let rest = data;
        while (rest.length > 0) {
            const altScreenMatch = rest.match(/^(\x1b\[\?1049[hl])/);
            if (altScreenMatch) {
                // Drop alternate-screen-buffer sequences so the terminal
                // scrollback remains visible instead of being hidden.
                rest = rest.slice(altScreenMatch[1].length);
                continue;
            }
            const setupMatch = rest.match(/^(\x1b\[(?:\?25[hl]|[0-9;]*[Hf]|[0-9;]*[JK]|2K))/);
            const resetMatch = rest.match(/^(\x1bc)/);
            const charsetMatch = rest.match(/^(\x1b\([A-Za-z0-9])/);
            const match = setupMatch ?? resetMatch ?? charsetMatch;
            if (!match)
                break;
            process.stdout.write(match[1]);
            rest = rest.slice(match[1].length);
        }
        return rest;
    }
    shouldContinueBuffering(clean) {
        const trimmed = clean.trim();
        if (trimmed.length === 0)
            return true;
        if (this.hasWelcomeLine(clean) || this.hasMetaLines(clean))
            return true;
        // Prompt-like lines should pass through immediately.
        if (/^kimi❯/.test(trimmed))
            return false;
        if (clean.split(/\r?\n/).some((line) => /^kimi❯/.test(line.trim())))
            return false;
        // Keep buffering when box frame is detected but welcome/meta text
        // hasn't arrived yet — process() will trigger replacement when complete.
        if (this.hasCompleteBox(clean))
            return true;
        return true;
    }
    /** 버퍼에 Kimi CLI 배너 시그널과 배너 상단(╭)/하단(╰)이 모두 존재하면 완료로 판단 */
    isBannerComplete(clean) {
        const hasWelcome = this.hasWelcomeLine(clean);
        const hasMeta = this.hasMetaLines(clean);
        if (!hasWelcome && !hasMeta) {
            return false;
        }
        // Require the box frame to confirm we have the full banner block.
        return this.hasCompleteBox(clean);
    }
    hasWelcomeLine(clean) {
        // Broad match: any Kimi CLI welcome / branding line
        return /Welcome\s+to\s+Kimi/i.test(clean)
            || /\bKimi\s+(?:Code\s+)?CLI\b/i.test(clean)
            || /\bKimi\b.*\b(?:Code|CLI|Chat)\b/i.test(clean);
    }
    hasMetaLines(clean) {
        return /Directory:|Session:|Model:/.test(clean);
    }
    hasCompleteBox(clean) {
        const lines = clean.split(/\r?\n/);
        const topChars = ["╭", "╔", "┌", "▛", "┏"];
        const bottomChars = ["╰", "╚", "└", "▙", "┗"];
        let sawTop = false;
        for (const line of lines) {
            const trimmed = line.trim();
            if (!sawTop && topChars.some((c) => trimmed.startsWith(c))) {
                sawTop = true;
                continue;
            }
            if (sawTop && bottomChars.some((c) => trimmed.startsWith(c))) {
                return true;
            }
        }
        return false;
    }
    flushReplace() {
        if (this.state !== "buffering")
            return null;
        this.clearTimeout();
        const meta = this.extractMeta(this.buffer);
        this.onMeta?.(meta);
        if (this.stripOnly) {
            this.state = "stripped";
        }
        else {
            this.state = "replaced";
            this.onReplace(meta);
        }
        let after = this.extractAfterBanner(this.buffer);
        if (after === null) {
            after = this.stripKimiBanner(this.buffer);
        }
        else {
            // Also strip any trailing banner fragments that may have been buffered
            after = this.stripKimiBanner(after);
        }
        this.chunks = [];
        this.cacheDirty = true;
        return after.length > 0 ? after : null;
    }
    flushPassthrough() {
        if (this.state !== "buffering")
            return null;
        this.state = "passthrough";
        this.clearTimeout();
        let result = this.stripKimiBanner(this.buffer);
        result = this.stripKimiBanner(result);
        this.chunks = [];
        this.cacheDirty = true;
        return result.length > 0 ? result : null;
    }
    extractMeta(_buf) {
        const meta = {};
        const clean = this.getStripped();
        for (const line of clean.split(/\r?\n/)) {
            const stripped = line.replace(/[│║┃╔╗╚╝╭╮╰╯─═]/g, "").trim();
            const dirMatch = stripped.match(/Directory:\s*(.+)/);
            if (dirMatch)
                meta.directory = dirMatch[1].trim();
            const sesMatch = stripped.match(/(?:Kimi\s+)?Session(?:\s*ID)?\s*:\s*(.+)/i);
            if (sesMatch)
                meta.session = sesMatch[1].trim();
            const modelMatch = stripped.match(/Model:\s*(.+)/);
            if (modelMatch)
                meta.model = modelMatch[1].trim();
        }
        return meta;
    }
    /** 배너 하단(╰─) 이후의 데이터만 추출 */
    extractAfterBanner(buf) {
        const origLines = buf.split(/\r?\n/);
        const cleanLines = this.getStripped().split(/\r?\n/);
        const bottomChars = ["╰", "╚", "└", "▙", "┗"];
        let endIdx = -1;
        for (let i = 0; i < cleanLines.length; i++) {
            const trimmed = cleanLines[i].trim();
            if (bottomChars.some((c) => trimmed.startsWith(c) || trimmed.includes(c + "─"))) {
                if (!bottomChars.some((_c) => trimmed.includes("╭") || trimmed.includes("╔") || trimmed.includes("┌"))) {
                    endIdx = i;
                    break;
                }
            }
        }
        if (endIdx === -1)
            return null;
        const after = origLines.slice(endIdx + 1);
        const result = after.join("\n");
        return result.length > 0 ? result : null;
    }
    /**
     * Fallback banner stripper: removes the Kimi CLI default banner block
     * (╭...╰) when the precise extractAfterBanner fails or we timeout.
     */
    stripKimiBanner(buf) {
        const lines = buf.split(/\r?\n/);
        const topChars = ["╭", "╔", "┌", "▛", "┏"];
        const bottomChars = ["╰", "╚", "└", "▙", "┗"];
        let startIdx = -1;
        let endIdx = -1;
        for (let i = 0; i < lines.length; i++) {
            const stripped = stripAnsi(lines[i]).trim();
            if (startIdx === -1 && topChars.some((c) => stripped.startsWith(c) || stripped.includes(c + "─"))) {
                startIdx = i;
            }
            if (startIdx !== -1 && bottomChars.some((c) => stripped.startsWith(c) || stripped.includes(c + "─"))) {
                if (!topChars.some((c) => stripped.includes(c))) {
                    endIdx = i;
                    break;
                }
            }
        }
        if (startIdx !== -1 && endIdx !== -1 && this.looksLikeKimiBanner(lines, startIdx, endIdx)) {
            return lines.slice(0, startIdx).concat(lines.slice(endIdx + 1)).join("\n");
        }
        return buf;
    }
    looksLikeKimiBanner(lines, start, end) {
        const block = lines.slice(start, end + 1).join("\n");
        const clean = stripAnsi(block);
        return /Welcome\s+to\s+Kimi/i.test(clean)
            || /\bKimi\s+(?:Code\s+)?CLI\b/i.test(clean)
            || (/Directory:/.test(clean) && /Session:/.test(clean));
    }
}
