/**
 * OutputManager - Handles all CLI output and streaming
 *
 * This manager is responsible for:
 * - Writing messages to stdout/stderr
 * - Tracking what's been displayed (to avoid duplicates)
 * - Managing streaming content with delta computation
 * - Formatting different message types appropriately
 *
 * Design notes:
 * - Uses the Observable pattern from client/events.ts for internal state
 * - Single responsibility: CLI output only (no prompting, no state detection)
 * - Can be disabled for TUI mode where Ink controls the terminal
 */

import { ClineMessage, ClineSay } from "@roo-code/types"

import { Observable } from "./events.js"

// =============================================================================
// Types
// =============================================================================

/**
 * Tracks what we've displayed for a specific message ts.
 */
export interface DisplayedMessage {
	ts: number
	text: string
	partial: boolean
}

/**
 * Tracks streaming state for a message.
 */
export interface StreamState {
	ts: number
	text: string
	headerShown: boolean
}

/**
 * Configuration options for OutputManager.
 */
export interface OutputManagerOptions {
	/**
	 * When true, completely disables all output.
	 * Use for TUI mode where another system controls the terminal.
	 */
	disabled?: boolean

	/**
	 * Stream for normal output (default: process.stdout).
	 */
	stdout?: NodeJS.WriteStream

	/**
	 * Stream for error output (default: process.stderr).
	 */
	stderr?: NodeJS.WriteStream
}

// =============================================================================
// OutputManager Class
// =============================================================================

export class OutputManager {
	private disabled: boolean
	private stdout: NodeJS.WriteStream
	private stderr: NodeJS.WriteStream

	/**
	 * Track displayed messages by ts to avoid duplicate output.
	 * Observable pattern allows external systems to subscribe if needed.
	 */
	private displayedMessages = new Map<number, DisplayedMessage>()

	/**
	 * Track streamed content by ts for delta computation.
	 */
	private streamedContent = new Map<number, StreamState>()

	/**
	 * Track which ts is currently streaming (for newline management).
	 */
	private currentlyStreamingTs: number | null = null

	/**
	 * Track whether a say:completion_result has been streamed,
	 * so the subsequent ask:completion_result doesn't duplicate the text.
	 */
	private completionResultStreamed = false

	/**
	 * Track first partial logs (for debugging first/last pattern).
	 */
	private loggedFirstPartial = new Set<number>()

	/**
	 * Observable for streaming state changes.
	 * External systems can subscribe to know when streaming starts/ends.
	 */
	public readonly streamingState = new Observable<{ ts: number | null; isStreaming: boolean }>({
		ts: null,
		isStreaming: false,
	})

	constructor(options: OutputManagerOptions = {}) {
		this.disabled = options.disabled ?? false
		this.stdout = options.stdout ?? process.stdout
		this.stderr = options.stderr ?? process.stderr
	}

	// ===========================================================================
	// Public API
	// ===========================================================================

	/**
	 * Output a ClineMessage based on its type.
	 * This is the main entry point for message output.
	 *
	 * @param msg - The message to output
	 * @param skipFirstUserMessage - If true, skip the first "text" message (user prompt echo)
	 */
	outputMessage(msg: ClineMessage, skipFirstUserMessage = true): void {
		const ts = msg.ts
		const text = msg.text || ""
		const isPartial = msg.partial === true
		const previousDisplay = this.displayedMessages.get(ts)
		const alreadyDisplayedComplete = previousDisplay && !previousDisplay.partial

		if (msg.type === "say" && msg.say) {
			this.outputSayMessage(ts, msg.say, text, isPartial, alreadyDisplayedComplete, skipFirstUserMessage)
		} else if (msg.type === "ask" && msg.ask) {
			// For ask messages, we only output command_output here
			// Other asks are handled by AskDispatcher
			if (msg.ask === "command_output") {
				this.outputCommandOutput(ts, text, isPartial, alreadyDisplayedComplete)
			}
		}
	}

	/**
	 * Output a simple text line with a label.
	 */
	output(label: string, text?: string): void {
		if (this.disabled) return
		const message = text ? `${label} ${text}\n` : `${label}\n`
		this.stdout.write(message)
	}

	/**
	 * Output an error message.
	 */
	outputError(label: string, text?: string): void {
		if (this.disabled) return
		const message = text ? `${label} ${text}\n` : `${label}\n`
		this.stderr.write(message)
	}

	/**
	 * Write raw text to stdout (for streaming).
	 */
	writeRaw(text: string): void {
		if (this.disabled) return
		this.stdout.write(text)
	}

	/**
	 * Check if a message has already been fully displayed.
	 */
	isAlreadyDisplayed(ts: number): boolean {
		const displayed = this.displayedMessages.get(ts)
		return displayed !== undefined && !displayed.partial
	}

	/**
	 * Check if we're currently streaming any message.
	 */
	isCurrentlyStreaming(): boolean {
		return this.currentlyStreamingTs !== null
	}

	/**
	 * Get the ts of the currently streaming message.
	 */
	getCurrentlyStreamingTs(): number | null {
		return this.currentlyStreamingTs
	}

	/**
	 * Mark a message as displayed (useful for external coordination).
	 */
	markDisplayed(ts: number, text: string, partial: boolean): void {
		this.displayedMessages.set(ts, { ts, text, partial })
	}

	/**
	 * Clear all tracking state.
	 * Call this when starting a new task.
	 */
	clear(): void {
		this.displayedMessages.clear()
		this.streamedContent.clear()
		this.currentlyStreamingTs = null
		this.completionResultStreamed = false
		this.loggedFirstPartial.clear()
		this.streamingState.next({ ts: null, isStreaming: false })
	}

	/**
	 * Get debugging info about first partial logging.
	 */
	hasLoggedFirstPartial(ts: number): boolean {
		return this.loggedFirstPartial.has(ts)
	}

	/**
	 * Record that we've logged the first partial for a ts.
	 */
	setLoggedFirstPartial(ts: number): void {
		this.loggedFirstPartial.add(ts)
	}

	/**
	 * Clear the first partial record (when complete).
	 */
	clearLoggedFirstPartial(ts: number): void {
		this.loggedFirstPartial.delete(ts)
	}

	// ===========================================================================
	// Say Message Output
	// ===========================================================================

	private outputSayMessage(
		ts: number,
		say: ClineSay,
		text: string,
		isPartial: boolean,
		alreadyDisplayedComplete: boolean | undefined,
		skipFirstUserMessage: boolean,
	): void {
		switch (say) {
			case "text":
				this.outputTextMessage(ts, text, isPartial, alreadyDisplayedComplete, skipFirstUserMessage)
				break

			// case "thinking": - not a valid ClineSay type
			case "reasoning":
				this.outputReasoningMessage(ts, text, isPartial, alreadyDisplayedComplete)
				break

			case "command_output":
				this.outputCommandOutput(ts, text, isPartial, alreadyDisplayedComplete)
				break

			case "completion_result":
				// completion_result can arrive as both a "say" (with streamed text)
				// and an "ask" (handled via TaskCompleted in extension-host.ts).
				// Stream the say variant here; the ask variant is handled by
				// outputCompletionResult which will skip if already displayed.
				this.outputCompletionSayMessage(ts, text, isPartial, alreadyDisplayedComplete)
				break

			case "error":
				if (!alreadyDisplayedComplete) {
					this.outputError("\n[error]", text || "Unknown error")
					this.displayedMessages.set(ts, { ts, text: text || "", partial: false })
				}
				break

			case "api_req_started":
				// Silent - no output needed
				break

			default:
				// NO-OP for unknown say types
				break
		}
	}

	private outputTextMessage(
		ts: number,
		text: string,
		isPartial: boolean,
		alreadyDisplayedComplete: boolean | undefined,
		skipFirstUserMessage: boolean,
	): void {
		// Skip the initial user prompt echo (first message with no prior messages)
		if (skipFirstUserMessage && this.displayedMessages.size === 0 && !this.displayedMessages.has(ts)) {
			this.displayedMessages.set(ts, { ts, text, partial: !!isPartial })
			return
		}

		if (isPartial && text) {
			// Stream partial content
			this.streamContent(ts, text, "[assistant]")
			this.displayedMessages.set(ts, { ts, text, partial: true })
		} else if (!isPartial && text && !alreadyDisplayedComplete) {
			// Message complete - ensure all content is output
			const streamed = this.streamedContent.get(ts)

			if (streamed) {
				// We were streaming - output any remaining delta and finish
				if (text.length > streamed.text.length && text.startsWith(streamed.text)) {
					const delta = text.slice(streamed.text.length)
					this.writeRaw(delta)
				}
				this.finishStream(ts)
			} else {
				// Not streamed yet - output complete message
				this.output("\n[assistant]", text)
			}

			this.displayedMessages.set(ts, { ts, text, partial: false })
			this.streamedContent.set(ts, { ts, text, headerShown: true })
		}
	}

	private outputReasoningMessage(
		ts: number,
		text: string,
		isPartial: boolean,
		alreadyDisplayedComplete: boolean | undefined,
	): void {
		if (isPartial && text) {
			this.streamContent(ts, text, "[reasoning]")
			this.displayedMessages.set(ts, { ts, text, partial: true })
		} else if (!isPartial && text && !alreadyDisplayedComplete) {
			// Reasoning complete - finish the stream
			const streamed = this.streamedContent.get(ts)

			if (streamed) {
				if (text.length > streamed.text.length && text.startsWith(streamed.text)) {
					const delta = text.slice(streamed.text.length)
					this.writeRaw(delta)
				}
				this.finishStream(ts)
			} else {
				this.output("\n[reasoning]", text)
			}

			this.displayedMessages.set(ts, { ts, text, partial: false })
		}
	}

	/**
	 * Output command_output (shared between say and ask types).
	 */
	outputCommandOutput(
		ts: number,
		text: string,
		isPartial: boolean,
		alreadyDisplayedComplete: boolean | undefined,
	): void {
		if (isPartial && text) {
			this.streamContent(ts, text, "[command output]")
			this.displayedMessages.set(ts, { ts, text, partial: true })
		} else if (!isPartial && text && !alreadyDisplayedComplete) {
			const streamed = this.streamedContent.get(ts)

			if (streamed) {
				if (text.length > streamed.text.length && text.startsWith(streamed.text)) {
					const delta = text.slice(streamed.text.length)
					this.writeRaw(delta)
				}
				this.finishStream(ts)
			} else {
				this.writeRaw("\n[command output] ")
				this.writeRaw(text)
				this.writeRaw("\n")
			}

			this.displayedMessages.set(ts, { ts, text, partial: false })
			this.streamedContent.set(ts, { ts, text, headerShown: true })
		}
	}

	// ===========================================================================
	// Streaming Helpers
	// ===========================================================================

	/**
	 * Stream content with delta computation - only output new characters.
	 */
	streamContent(ts: number, text: string, header: string): void {
		const previous = this.streamedContent.get(ts)

		if (!previous) {
			// First time seeing this message - output header and initial text
			this.writeRaw(`\n${header} `)
			this.writeRaw(text)
			this.streamedContent.set(ts, { ts, text, headerShown: true })
			this.currentlyStreamingTs = ts
			this.streamingState.next({ ts, isStreaming: true })
		} else if (text.length > previous.text.length && text.startsWith(previous.text)) {
			// Text has grown - output delta
			const delta = text.slice(previous.text.length)
			this.writeRaw(delta)
			this.streamedContent.set(ts, { ts, text, headerShown: true })
		}
	}

	/**
	 * Finish streaming a message (add newline).
	 */
	finishStream(ts: number): void {
		if (this.currentlyStreamingTs === ts) {
			this.writeRaw("\n")
			this.currentlyStreamingTs = null
			this.streamingState.next({ ts: null, isStreaming: false })
		}
	}

	/**
	 * Output a say:completion_result message (streamed text of the completion).
	 * The subsequent ask:completion_result is handled by outputCompletionResult.
	 */
	private outputCompletionSayMessage(
		ts: number,
		text: string,
		isPartial: boolean,
		alreadyDisplayedComplete: boolean | undefined,
	): void {
		if (isPartial && text) {
			this.streamContent(ts, text, "[assistant]")
			this.displayedMessages.set(ts, { ts, text, partial: true })
			this.completionResultStreamed = true
		} else if (!isPartial && text && !alreadyDisplayedComplete) {
			const streamed = this.streamedContent.get(ts)

			if (streamed) {
				if (text.length > streamed.text.length && text.startsWith(streamed.text)) {
					const delta = text.slice(streamed.text.length)
					this.writeRaw(delta)
				}
				this.finishStream(ts)
			} else {
				this.output("\n[assistant]", text)
			}

			this.displayedMessages.set(ts, { ts, text, partial: false })
			this.completionResultStreamed = true
		}
	}

	/**
	 * Output completion message (called from TaskCompleted handler).
	 */
	outputCompletionResult(ts: number, text: string): void {
		const previousDisplay = this.displayedMessages.get(ts)
		if (!previousDisplay || previousDisplay.partial) {
			if (this.completionResultStreamed) {
				// Text was already streamed via say:completion_result.
				this.output("\n[task complete]")
			} else {
				this.output("\n[task complete]", text || "")
			}
			this.displayedMessages.set(ts, { ts, text: text || "", partial: false })
		}
	}
}
