/**
 * ProviderEventNormalizer — converts raw provider events into normalized OmkEvent types.
 *
 * Two concrete implementations:
 *   KimiEventNormalizer  — structured wire-protocol events (kimi --wire)
 *   KimiPrintNormalizer  — raw stdout text chunks  (kimi --print)
 *
 * The normalizer never leaks raw provider output to the user; that is
 * OutputRouter's responsibility.  The normalizer only shapes the stream.
 */
import { t } from "../util/i18n.js";
// ───────────────────────────────────────────────────────────────
// KimiEventNormalizer — structured wire-protocol events
// ───────────────────────────────────────────────────────────────
/**
 * Normalizes Kimi wire-protocol events (JSON-RPC 2.0 messages from kimi --wire).
 *
 * Input shapes (WireEvent union from wire-protocol-types.ts):
 *   { type: "TurnBegin",       payload: TurnBegin }
 *   { type: "TurnEnd" }
 *   { type: "StepBegin",       payload: StepBegin }
 *   { type: "StepInterrupted" }
 *   { type: "CompactionBegin" }
 *   { type: "CompactionEnd" }
 *   { type: "StatusUpdate",    payload: StatusUpdate }
 *   { type: "ContentPart",     payload: TextPart | ThinkPart | … }
 *   { type: "ToolCall",        payload: ToolCallEvent }
 *   { type: "ToolCallPart",    payload: ToolCallPart }
 *   { type: "ToolResult",      payload: ToolResultEvent }
 *   { type: "SubagentEvent",   payload: SubagentEvent }
 *   { type: "PlanDisplay",     payload: PlanDisplay }
 *   { type: "HookTriggered",   payload: HookTriggered }
 *   { type: "HookResolved",    payload: HookResolved }
 *   { type: "ApprovalResponse", payload: ApprovalResponseEvent }
 *   { type: "SteerInput",      payload: SteerInput }
 */
export class KimiEventNormalizer {
    listeners = [];
    turnId;
    startedAt = 0;
    stepCount = 0;
    accumulatedText = "";
    toolCallCount = 0;
    constructor(turnId) {
        this.turnId = turnId;
    }
    onEvent(listener) {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter((l) => l !== listener);
        };
    }
    push(raw) {
        const msg = raw;
        if (typeof msg?.type !== "string")
            return;
        switch (msg.type) {
            case "TurnBegin": {
                this.startedAt = Date.now();
                this.emit("turn_started", {
                    kind: "turn_started",
                    intent: extractUserInput(msg.payload),
                    provider: "kimi-wire",
                });
                break;
            }
            case "TurnEnd": {
                // TurnEnd is a no-op here; finalize() handles turn_finished.
                break;
            }
            case "StepBegin": {
                this.stepCount++;
                const n = msg.payload?.n ?? this.stepCount;
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.step", String(n)),
                });
                break;
            }
            case "StepInterrupted": {
                this.emit("warning", {
                    kind: "warning",
                    message: t("normalizer.stepInterrupted"),
                    code: "step_interrupted",
                });
                break;
            }
            case "CompactionBegin": {
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.compacting"),
                });
                break;
            }
            case "CompactionEnd": {
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.compactionComplete"),
                });
                break;
            }
            case "StatusUpdate": {
                const payload = msg.payload;
                if (payload?.token_usage) {
                    this.emit("progress", {
                        kind: "progress",
                        message: t("normalizer.tokens", String(payload.token_usage.input_other), String(payload.token_usage.output)),
                    });
                }
                if (payload?.plan_mode != null) {
                    this.emit("mcp_status", {
                        kind: "mcp_status",
                        server: "plan-mode",
                        status: payload.plan_mode ? "connected" : "skipped",
                    });
                }
                break;
            }
            case "ContentPart": {
                const payload = msg.payload;
                if (payload?.type === "text" && typeof payload.text === "string") {
                    this.accumulatedText += payload.text;
                    this.emit("progress", {
                        kind: "progress",
                        message: payload.text,
                    });
                }
                // ThinkPart is metadata only — not surfaced as an OmkEvent.
                break;
            }
            case "ToolCall": {
                this.toolCallCount++;
                const payload = msg.payload;
                const name = payload?.function?.name ?? "unknown";
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.toolCall", name),
                });
                break;
            }
            case "ToolCallPart": {
                // Streaming argument fragment — too granular for OmkEvent.
                break;
            }
            case "ToolResult": {
                const payload = msg.payload;
                if (payload?.return_value?.is_error) {
                    this.emit("warning", {
                        kind: "warning",
                        message: t("normalizer.toolError", String(payload.return_value.output).slice(0, 200)),
                        code: "tool_error",
                    });
                }
                break;
            }
            case "SubagentEvent": {
                const payload = msg.payload;
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.subagent", payload?.subagent_type ?? payload?.agent_id ?? "unknown"),
                });
                break;
            }
            case "PlanDisplay": {
                const payload = msg.payload;
                this.emit("result", {
                    kind: "result",
                    content: payload?.content ?? "",
                    format: "markdown",
                });
                break;
            }
            case "HookTriggered": {
                const payload = msg.payload;
                this.emit("mcp_status", {
                    kind: "mcp_status",
                    server: `hook:${payload?.event ?? "unknown"}`,
                    status: "connected",
                });
                break;
            }
            case "HookResolved": {
                const payload = msg.payload;
                if (payload?.action === "block") {
                    this.emit("warning", {
                        kind: "warning",
                        message: t("normalizer.hookBlocked", payload.reason ?? "no reason"),
                        code: "hook_blocked",
                    });
                }
                break;
            }
            case "ApprovalResponse": {
                // Approval responses are handled by the wire client directly.
                break;
            }
            case "SteerInput": {
                this.emit("progress", {
                    kind: "progress",
                    message: t("normalizer.steerInput"),
                });
                break;
            }
            default: {
                this.emit("warning", {
                    kind: "warning",
                    message: t("normalizer.unknownWireEvent", msg.type),
                    code: "unknown_wire_event",
                });
            }
        }
    }
    finalize(outcome) {
        if (this.accumulatedText.length > 0) {
            this.emit("result", {
                kind: "result",
                content: this.accumulatedText,
                format: "text",
            });
        }
        this.emit("turn_finished", {
            kind: "turn_finished",
            durationMs: outcome.durationMs,
            tokenUsage: outcome.tokenUsage,
        });
    }
    reset() {
        this.startedAt = 0;
        this.stepCount = 0;
        this.accumulatedText = "";
        this.toolCallCount = 0;
    }
    // ── internals ──
    emit(type, data) {
        const event = {
            type,
            timestamp: new Date().toISOString(),
            turnId: this.turnId,
            data,
        };
        for (const listener of this.listeners) {
            try {
                listener(event);
            }
            catch {
                // Listeners must not throw; swallow silently.
            }
        }
    }
}
// ───────────────────────────────────────────────────────────────
// KimiPrintNormalizer — raw stdout text chunks
// ───────────────────────────────────────────────────────────────
/**
 * Normalizes raw stdout lines from `kimi --print` (execa child process).
 *
 * The print runtime emits plain text — no structured events.
 * This normalizer buffers lines and emits progress/result OmkEvents.
 *
 * Special markers it recognizes:
 *   [omk_mcp:<server>:<status>]   → mcp_status
 *   [ERROR] ...                   → error
 *   [WARN] ...                    → warning
 *   [omk_tool:<name>]             → progress (tool call indicator)
 */
export class KimiPrintNormalizer {
    listeners = [];
    turnId;
    startedAt = 0;
    buffer = "";
    // Pattern: [omk_mcp:<server>:<status>]
    static MCP_RE = /^\[omk_mcp:([^:]+):(\w+)\]\s*/;
    // Pattern: [ERROR] or [WARN]
    static ERROR_RE = /^\[ERROR\]\s*/;
    static WARN_RE = /^\[WARN\]\s*/;
    // Pattern: [omk_tool:<name>]
    static TOOL_RE = /^\[omk_tool:([^\]]+)\]\s*/;
    constructor(turnId) {
        this.turnId = turnId;
    }
    onEvent(listener) {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter((l) => l !== listener);
        };
    }
    /**
     * Push a raw text chunk (may contain multiple lines).
     * The print runtime delivers stdout in arbitrary chunks.
     */
    push(raw) {
        if (typeof raw !== "string")
            return;
        this.buffer += raw;
        const lines = this.buffer.split("\n");
        // Keep the last incomplete line in the buffer.
        this.buffer = lines.pop() ?? "";
        for (const line of lines) {
            this.processLine(line);
        }
    }
    finalize(outcome) {
        // Flush any remaining buffer content.
        if (this.buffer.length > 0) {
            this.processLine(this.buffer);
            this.buffer = "";
        }
        this.emit("turn_finished", {
            kind: "turn_finished",
            durationMs: outcome.durationMs,
            tokenUsage: outcome.tokenUsage,
        });
    }
    reset() {
        this.buffer = "";
        this.startedAt = 0;
    }
    // ── internals ──
    processLine(line) {
        if (this.startedAt === 0)
            this.startedAt = Date.now();
        // Check for MCP status marker.
        const mcpMatch = line.match(KimiPrintNormalizer.MCP_RE);
        if (mcpMatch) {
            const server = mcpMatch[1];
            const status = mcpMatch[2];
            const remainder = line.slice(mcpMatch[0].length);
            this.emit("mcp_status", {
                kind: "mcp_status",
                server,
                status: normalizeMcpStatus(status),
            });
            if (remainder.length > 0) {
                this.emit("progress", { kind: "progress", message: remainder });
            }
            return;
        }
        // Check for tool call marker.
        const toolMatch = line.match(KimiPrintNormalizer.TOOL_RE);
        if (toolMatch) {
            const toolName = toolMatch[1];
            const remainder = line.slice(toolMatch[0].length);
            this.emit("progress", {
                kind: "progress",
                message: t("normalizer.toolCall", toolName),
            });
            if (remainder.length > 0) {
                this.emit("progress", { kind: "progress", message: remainder });
            }
            return;
        }
        // Check for error marker.
        if (KimiPrintNormalizer.ERROR_RE.test(line)) {
            const message = line.replace(KimiPrintNormalizer.ERROR_RE, "");
            this.emit("error", {
                kind: "error",
                message,
                code: "provider_error",
                recoverable: true,
            });
            return;
        }
        // Check for warning marker.
        if (KimiPrintNormalizer.WARN_RE.test(line)) {
            const message = line.replace(KimiPrintNormalizer.WARN_RE, "");
            this.emit("warning", {
                kind: "warning",
                message,
                code: "provider_warning",
            });
            return;
        }
        // Default: treat as progress text.
        this.emit("progress", { kind: "progress", message: line });
    }
    emit(type, data) {
        const event = {
            type,
            timestamp: new Date().toISOString(),
            turnId: this.turnId,
            data,
        };
        for (const listener of this.listeners) {
            try {
                listener(event);
            }
            catch {
                // Listeners must not throw; swallow silently.
            }
        }
    }
}
// ───────────────────────────────────────────────────────────────
// Factory
// ───────────────────────────────────────────────────────────────
/**
 * Create a ProviderEventNormalizer for the given provider name.
 *
 * Supported providers:
 *   "kimi-wire"   → KimiEventNormalizer  (structured JSON-RPC events)
 *   "kimi-print"  → KimiPrintNormalizer   (raw stdout text)
 *   "kimi"        → KimiEventNormalizer   (alias for wire)
 *   "deepseek"    → KimiPrintNormalizer   (advisory uses print-style stdout)
 *   "codex"       → KimiPrintNormalizer   (CLI stdout)
 *   "gemini"      → KimiPrintNormalizer   (CLI stdout)
 *   "claude"      → KimiPrintNormalizer   (CLI stdout)
 *
 * Unknown providers default to KimiPrintNormalizer (stdout-based).
 */
export function createProviderEventNormalizer(provider, turnId) {
    switch (provider) {
        case "kimi-wire":
        case "kimi":
            return new KimiEventNormalizer(turnId);
        case "kimi-print":
        case "deepseek":
        case "codex":
        case "gemini":
        case "claude":
        default:
            return new KimiPrintNormalizer(turnId);
    }
}
// ───────────────────────────────────────────────────────────────
// Helpers
// ───────────────────────────────────────────────────────────────
function extractUserInput(payload) {
    const input = payload?.user_input;
    if (typeof input === "string")
        return input;
    if (Array.isArray(input)) {
        const textParts = input.filter((p) => typeof p === "object" && p !== null && p.type === "text");
        return textParts
            .map((p) => p.text ?? "")
            .join("");
    }
    return "";
}
function normalizeMcpStatus(raw) {
    switch (raw) {
        case "connected":
        case "ok":
        case "ready":
            return "connected";
        case "failed":
        case "error":
            return "failed";
        case "skipped":
        case "disabled":
            return "skipped";
        default:
            return "connected";
    }
}
