import { truncateOutput, applyRunLengthEncoding } from "../misc/extract-text"

import type {
	RooTerminalProvider,
	RooTerminal,
	RooTerminalCallbacks,
	RooTerminalProcess,
	RooTerminalProcessResultPromise,
	ExitCodeDetails,
} from "./types"

export abstract class BaseTerminal implements RooTerminal {
	public readonly provider: RooTerminalProvider
	public readonly id: number
	public readonly initialCwd: string
	public readonly createdAt: number

	public busy: boolean
	public running: boolean
	protected streamClosed: boolean

	public taskId?: string
	public process?: RooTerminalProcess
	public completedProcesses: RooTerminalProcess[] = []

	constructor(provider: RooTerminalProvider, id: number, cwd: string) {
		this.provider = provider
		this.id = id
		this.initialCwd = cwd
		this.createdAt = Date.now()
		this.busy = false
		this.running = false
		this.streamClosed = false
	}

	public getCurrentWorkingDirectory(): string {
		return this.initialCwd
	}

	abstract isClosed(): boolean

	abstract runCommand(command: string, callbacks: RooTerminalCallbacks): RooTerminalProcessResultPromise

	/**
	 * Sets the active stream for this terminal and notifies the process
	 * @param stream The stream to set, or undefined to clean up
	 * @throws Error if process is undefined when a stream is provided
	 */
	public async setActiveStream(stream: AsyncIterable<string> | undefined, pid: Thenable<number | undefined>) {
		if (stream) {
			if (!this.process) {
				this.running = false

				console.warn(
					`[Terminal ${this.provider}/${this.id}] process is undefined, so cannot set terminal stream (probably user-initiated non-CoStrict command)`,
				)

				return
			}

			this.running = true
			this.streamClosed = false
			this.process.emit("shell_execution_started", await pid)
			this.process.emit("stream_available", stream)
		} else {
			this.streamClosed = true
		}
	}

	/**
	 * Handles shell execution completion for this terminal.
	 * @param exitDetails The exit details of the shell execution
	 */
	public shellExecutionComplete(exitDetails: ExitCodeDetails) {
		this.busy = false
		this.running = false

		if (this.process) {
			// Add to the front of the queue (most recent first).
			if (this.process.hasUnretrievedOutput()) {
				this.completedProcesses.unshift(this.process)
			}

			this.process.emit("shell_execution_complete", exitDetails)
			this.process = undefined
		}
	}

	public get isStreamClosed(): boolean {
		return this.streamClosed
	}

	/**
	 * Gets the last executed command
	 * @returns The last command string or empty string if none
	 */
	public getLastCommand(): string {
		// Return the command from the active process or the most recent process in the queue
		if (this.process) {
			return this.process.command || ""
		} else if (this.completedProcesses.length > 0) {
			return this.completedProcesses[0].command || ""
		}

		return ""
	}

	/**
	 * Cleans the process queue by removing processes that no longer have unretrieved output
	 * or don't belong to the current task
	 */
	public cleanCompletedProcessQueue(): void {
		// Trim retrieved output from each process to free memory, then keep only those with remaining output
		this.completedProcesses = this.completedProcesses.filter((process) => {
			process.trimRetrievedOutput()
			return process.hasUnretrievedOutput()
		})
	}

	/**
	 * Gets all processes with unretrieved output
	 * @returns Array of processes with unretrieved output
	 */
	public getProcessesWithOutput(): RooTerminalProcess[] {
		// Clean the queue first to remove any processes without output
		this.cleanCompletedProcessQueue()
		return [...this.completedProcesses]
	}

	/**
	 * Gets all unretrieved output from both active and completed processes
	 * @returns Combined unretrieved output from all processes
	 */
	public getUnretrievedOutput(): string {
		let output = ""

		// First check completed processes to maintain chronological order
		for (const process of this.completedProcesses) {
			const processOutput = process.getUnretrievedOutput()

			if (processOutput) {
				output += processOutput
			}
		}

		// Then check active process for most recent output
		const activeOutput = this.process?.getUnretrievedOutput()

		if (activeOutput) {
			output += activeOutput
		}

		this.cleanCompletedProcessQueue()
		return output
	}

	/**
	 * Checks if the terminal has any unretrieved output
	 * @returns True if there is unretrieved output, false otherwise
	 */
	public hasUnretrievedOutput(): boolean {
		// Check completed processes first
		for (const process of this.completedProcesses) {
			if (process.hasUnretrievedOutput()) {
				return true
			}
		}

		// Then check active process
		return this.process?.hasUnretrievedOutput() ?? false
	}

	public static defaultShellIntegrationTimeout = 5_000
	private static shellIntegrationTimeout: number = BaseTerminal.defaultShellIntegrationTimeout
	private static shellIntegrationDisabled: boolean = false
	private static commandDelay: number = 0
	private static powershellCounter: boolean = false
	private static terminalZshClearEolMark: boolean = true
	private static terminalZshOhMy: boolean = false
	private static terminalZshP10k: boolean = false
	private static terminalZdotdir: boolean = false
	private static execaShellPath: string | undefined = undefined

	/**
	 * Compresses terminal output by applying run-length encoding and truncating to line limit
	 * @param input The terminal output to compress
	 * @returns The compressed terminal output
	 */
	public static setShellIntegrationTimeout(timeoutMs: number): void {
		BaseTerminal.shellIntegrationTimeout = timeoutMs
	}

	public static getShellIntegrationTimeout(): number {
		return BaseTerminal.shellIntegrationTimeout
	}

	public static setShellIntegrationDisabled(disabled: boolean): void {
		BaseTerminal.shellIntegrationDisabled = disabled
	}

	public static getShellIntegrationDisabled(): boolean {
		return BaseTerminal.shellIntegrationDisabled
	}

	/**
	 * Sets the command delay in milliseconds
	 * @param delayMs The delay in milliseconds
	 */
	public static setCommandDelay(delayMs: number): void {
		BaseTerminal.commandDelay = delayMs
	}

	/**
	 * Gets the command delay in milliseconds
	 * @returns The command delay in milliseconds
	 */
	public static getCommandDelay(): number {
		return BaseTerminal.commandDelay
	}

	/**
	 * Sets whether to use the PowerShell counter workaround
	 * @param enabled Whether to enable the PowerShell counter workaround
	 */
	public static setPowershellCounter(enabled: boolean): void {
		BaseTerminal.powershellCounter = enabled
	}

	/**
	 * Gets whether to use the PowerShell counter workaround
	 * @returns Whether the PowerShell counter workaround is enabled
	 */
	public static getPowershellCounter(): boolean {
		return BaseTerminal.powershellCounter
	}

	/**
	 * Sets whether to clear the ZSH EOL mark
	 * @param enabled Whether to clear the ZSH EOL mark
	 */
	public static setTerminalZshClearEolMark(enabled: boolean): void {
		BaseTerminal.terminalZshClearEolMark = enabled
	}

	/**
	 * Gets whether to clear the ZSH EOL mark
	 * @returns Whether the ZSH EOL mark clearing is enabled
	 */
	public static getTerminalZshClearEolMark(): boolean {
		return BaseTerminal.terminalZshClearEolMark
	}

	/**
	 * Sets whether to enable Oh My Zsh shell integration
	 * @param enabled Whether to enable Oh My Zsh shell integration
	 */
	public static setTerminalZshOhMy(enabled: boolean): void {
		BaseTerminal.terminalZshOhMy = enabled
	}

	/**
	 * Gets whether Oh My Zsh shell integration is enabled
	 * @returns Whether Oh My Zsh shell integration is enabled
	 */
	public static getTerminalZshOhMy(): boolean {
		return BaseTerminal.terminalZshOhMy
	}

	/**
	 * Sets whether to enable Powerlevel10k shell integration
	 * @param enabled Whether to enable Powerlevel10k shell integration
	 */
	public static setTerminalZshP10k(enabled: boolean): void {
		BaseTerminal.terminalZshP10k = enabled
	}

	/**
	 * Gets whether Powerlevel10k shell integration is enabled
	 * @returns Whether Powerlevel10k shell integration is enabled
	 */
	public static getTerminalZshP10k(): boolean {
		return BaseTerminal.terminalZshP10k
	}

	/**
	 * Compresses terminal output by applying run-length encoding and truncating to reasonable limits.
	 * Uses hardcoded defaults: 500 lines, 50K characters - these are UI display limits to prevent
	 * memory issues, not LLM context limits (which are controlled by terminalOutputPreviewSize).
	 * @param input The terminal output to compress
	 * @returns The compressed terminal output
	 */
	public static compressTerminalOutput(input: string): string {
		// Hardcoded UI display limits - these prevent unbounded memory growth
		// in the chat display, separate from the LLM context limits
		const LINE_LIMIT = 500
		const CHARACTER_LIMIT = 50_000

		return truncateOutput(applyRunLengthEncoding(input), LINE_LIMIT, CHARACTER_LIMIT)
	}

	/**
	 * Sets whether to enable ZDOTDIR handling for zsh
	 * @param enabled Whether to enable ZDOTDIR handling
	 */
	public static setTerminalZdotdir(enabled: boolean): void {
		BaseTerminal.terminalZdotdir = enabled
	}

	/**
	 * Gets whether ZDOTDIR handling is enabled
	 * @returns Whether ZDOTDIR handling is enabled
	 */
	public static getTerminalZdotdir(): boolean {
		return BaseTerminal.terminalZdotdir
	}

	public static setExecaShellPath(shellPath: string | undefined): void {
		BaseTerminal.execaShellPath = shellPath
	}

	public static getExecaShellPath(): string | undefined {
		return BaseTerminal.execaShellPath
	}
}
