import { execa, ExecaError, Options } from "execa"
import psTree from "ps-tree"
import process from "process"
import { getShell } from "../../utils/shell"

import type { RooTerminal } from "./types"
import { BaseTerminal } from "./BaseTerminal"
import { BaseTerminalProcess } from "./BaseTerminalProcess"
import { getIdeaShellEnvWithUpdatePath } from "../../utils/ideaShellEnvLoader"
import { isCliPatform, isJetbrainsPlatform } from "../../utils/platform"
import { t } from "../../i18n"
import delay from "delay"
import { isGbkEncodedCommand } from "./constants"

export class ExecaTerminalProcess extends BaseTerminalProcess {
	private terminalRef: WeakRef<RooTerminal>
	private aborted = false
	private pid?: number
	private subprocess?: ReturnType<typeof execa>
	private pidUpdatePromise?: Promise<void>

	constructor(terminal: RooTerminal) {
		super()

		this.terminalRef = new WeakRef(terminal)

		this.once("completed", () => {
			this.terminal.busy = false
		})
	}

	public get terminal(): RooTerminal {
		const terminal = this.terminalRef.deref()

		if (!terminal) {
			throw new Error("Unable to dereference terminal")
		}

		return terminal
	}

	public override async run(command: string) {
		this.command = command

		try {
			this.isHot = true

			this.subprocess = execa({
				shell: !isCliPatform() ? getShell() : BaseTerminal.getExecaShellPath() || true,
				cwd: this.terminal.getCurrentWorkingDirectory(),
				all: true,
				encoding: "buffer",
				// Ignore stdin to ensure non-interactive mode and prevent hanging
				stdin: "ignore",
				env: {
					...(isJetbrainsPlatform() ? getIdeaShellEnvWithUpdatePath(process.env) : process.env),
					// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
					LANG: "en_US.UTF-8",
					LC_ALL: "en_US.UTF-8",
					LANGUAGE: "en_US.UTF-8",
					PYTHONIOENCODING: "utf-8",
				},
			})`${command}`

			this.pid = this.subprocess.pid

			// When using shell: true, the PID is for the shell, not the actual command
			// Find the actual command PID after a small delay
			if (this.pid) {
				this.pidUpdatePromise = new Promise<void>((resolve) => {
					setTimeout(() => {
						psTree(this.pid!, (err, children) => {
							if (!err && children.length > 0) {
								// Update PID to the first child (the actual command)
								const actualPid = parseInt(children[0].PID)
								if (!isNaN(actualPid)) {
									this.pid = actualPid
								}
							}
							resolve()
						})
					}, 100)
				})
			}

			// Check if this is a background command (ends with &)
			const isBackgroundCommand = /&\s*(#.*)?$/.test(command.trim())
			const rawStream = this.subprocess.iterable({ from: "all", preserveNewlines: true })
			const useGbkEncoding = isGbkEncodedCommand(command)

			// Select the decoder based on the command type.
			const decoder = new TextDecoder(useGbkEncoding ? "gbk" : "utf-8", { fatal: false })
			const stream = (async function* () {
				for await (const chunk of rawStream) {
					if (typeof chunk === "string") {
						yield chunk
					} else {
						yield decoder.decode(chunk, { stream: true })
					}
				}
			})()

			await this.terminal.setActiveStream(stream, Promise.resolve(this.pid))
			let outputCount = 0
			delay(1500).then(() => {
				if (this.aborted || outputCount > 0) {
					return
				}

				const warning = `[${isBackgroundCommand ? "background " : ""}command running] ${command.length > 80 ? `${command.slice(0, 80)}...` : command}\n`
				this.emit("line", warning)
				this.startHotTimer(warning)
			})
			for await (const line of stream) {
				if (this.aborted) {
					break
				}
				if (outputCount < 3) outputCount++
				this.fullOutput += line

				const now = Date.now()

				if (
					this.isListening &&
					(now - this.lastEmitTime_ms > 1000 || this.lastEmitTime_ms === 0 || outputCount <= 3)
				) {
					this.emitRemainingBufferIfListening()
					this.lastEmitTime_ms = now
				}

				this.startHotTimer(line)
			}

			await delay(150)
			this.emitRemainingBufferIfListening()
			this.startHotTimer(this.fullOutput.slice(-2000))

			if (this.aborted) {
				let timeoutId: NodeJS.Timeout | undefined

				const kill = new Promise<void>((resolve) => {
					console.log(`[ExecaTerminalProcess#run] SIGKILL -> ${this.pid}`)

					timeoutId = setTimeout(() => {
						try {
							this.subprocess?.kill("SIGKILL")
						} catch (e) {}

						resolve()
					}, 5_000)
				})

				try {
					await Promise.race([this.subprocess, kill])
				} catch (error) {
					console.log(
						`[ExecaTerminalProcess#run] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`,
					)
				}

				if (timeoutId) {
					clearTimeout(timeoutId)
				}
			}

			this.emit("shell_execution_complete", { exitCode: 0 })
		} catch (error) {
			if (error instanceof ExecaError) {
				console.error(`[ExecaTerminalProcess#run] shell execution error: ${error.message}`)
				this.emit("shell_execution_complete", { exitCode: error.exitCode ?? 0, signalName: error.signal })
			} else {
				console.error(
					`[ExecaTerminalProcess#run] shell execution error: ${error instanceof Error ? error.message : String(error)}`,
				)

				this.emit("shell_execution_complete", { exitCode: 1 })
			}
			this.subprocess = undefined
		}

		await Promise.all([
			this.terminal.setActiveStream(undefined, Promise.resolve(this.pid)),
			this.emitRemainingBufferIfListening(),
		])
		this.stopHotTimer()
		this.emit("completed", this.fullOutput)
		this.emit("continue")
		this.subprocess = undefined
	}

	public override continue() {
		this.isListening = false
		this.removeAllListeners("line")
		this.emit("continue")
	}

	public userInput(input: string) {
		this.emit("line", `${input ? `${input}\n` : ""}`)
	}

	public override abort() {
		this.aborted = true

		// Function to perform the kill operations
		const performKill = () => {
			// Try to kill using the subprocess object
			if (this.subprocess) {
				try {
					this.subprocess.kill("SIGKILL")
				} catch (e) {
					console.warn(
						`[ExecaTerminalProcess#abort] Failed to kill subprocess: ${e instanceof Error ? e.message : String(e)}`,
					)
				}
			}

			// Kill the stored PID (which should be the actual command after our update)
			if (this.pid) {
				try {
					process.kill(this.pid, "SIGKILL")
				} catch (e) {
					if (e.code === "ESRCH") {
						const error = new Error(
							t("common:errors.command_esrch", { pid: this.pid, command: this.command }),
						)
						Object.assign(error, { __IS_ESRCH__: true })
						// this.emit("shell_execution_complete", { exitCode: e.exitCode ?? -1, signalName: e.signal ??  t("common:errors.command_esrch", { pid: this.pid, command: this.command })})
						this.emit("error", error)
					}
					console.warn(
						`[ExecaTerminalProcess#abort] Failed to kill process ${this.pid}: ${e instanceof Error ? e.message : String(e)}`,
					)
				}
			}
		}

		// If PID update is in progress, wait for it before killing
		if (this.pidUpdatePromise) {
			this.pidUpdatePromise.then(performKill).catch(() => performKill())
		} else {
			performKill()
		}

		// Continue with the rest of the abort logic
		if (this.pid) {
			// Also check for any child processes
			psTree(this.pid, async (err, children) => {
				if (!err) {
					const pids = children.map((p) => parseInt(p.PID))

					for (const pid of pids) {
						try {
							process.kill(pid, "SIGKILL")
						} catch (e) {
							console.warn(
								`[ExecaTerminalProcess#abort] Failed to send SIGKILL to child PID ${pid}: ${e instanceof Error ? e.message : String(e)}`,
							)
						}
					}
				} else {
					console.error(
						`[ExecaTerminalProcess#abort] Failed to get process tree for PID ${this.pid}: ${err.message}`,
					)
				}
			})
		}
	}

	public override hasUnretrievedOutput() {
		return this.lastRetrievedIndex < this.fullOutput.length
	}

	public override getUnretrievedOutput() {
		let output = this.fullOutput.slice(this.lastRetrievedIndex)
		let index = output.lastIndexOf("\n")

		if (index === -1) {
			return ""
		}

		index++
		this.lastRetrievedIndex += index

		return output.slice(0, index)
	}

	private emitRemainingBufferIfListening() {
		if (!this.isListening) {
			return
		}

		const output = this.getUnretrievedOutput()

		if (output !== "") {
			this.emit("line", output)
		}
	}
}
