import AppKit
import SwiftTerm
import Combine

/// Wraps a single SwiftTerm TerminalView + LocalProcess pair.
@MainActor
class TerminalSession: NSObject, ObservableObject, LocalProcessDelegate, @preconcurrency TerminalViewDelegate {

    let id = UUID()
    let terminalView: TerminalView
    private var process: LocalProcess?
    private var keyEventMonitor: Any?
    private let backgroundColor = NSColor.black
    private let configuredShellPath: String
    private let launchDirectory: String

    @Published var title: String = "shell"
    @Published var isRunning: Bool = true
    @Published var currentDirectory: String?

    init(
        fontSize: CGFloat,
        theme: TerminalTheme,
        shellPath: String,
        initialDirectory: String? = nil,
        startImmediately: Bool = true
    ) {
        terminalView = TerminalView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
        configuredShellPath = shellPath
        launchDirectory = Self.resolveInitialDirectory(initialDirectory)
        currentDirectory = launchDirectory
        super.init()

        terminalView.terminalDelegate = self

        let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
        terminalView.font = font
        applyTheme(theme)
        installCommandArrowMonitor()

        if startImmediately {
            startShell()
        } else {
            isRunning = false
        }
    }

    deinit {
        if let keyEventMonitor {
            NSEvent.removeMonitor(keyEventMonitor)
        }
    }

    // MARK: - Shell management

    private func startShell() {
        let shellPath = resolveShell()
        let shellName = (shellPath as NSString).lastPathComponent
        let loginExecName = "-\(shellName)"

        let proc = LocalProcess(delegate: self)
        // Launch as a login shell so user startup files initialize PATH/tools.
        proc.startProcess(
            executable: shellPath,
            args: ["-l"],
            environment: nil,
            execName: loginExecName,
            currentDirectory: launchDirectory
        )
        process = proc
        title = shellName
    }

    private static func resolveInitialDirectory(_ directory: String?) -> String {
        normalizedDirectory(directory) ?? NSHomeDirectory()
    }

    private static func normalizedDirectory(_ directory: String?) -> String? {
        let trimmed = directory?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !trimmed.isEmpty else {
            return nil
        }

        if let url = URL(string: trimmed), url.isFileURL {
            return url.path(percentEncoded: false)
        }

        return (trimmed as NSString).expandingTildeInPath
    }

    private func resolveShell() -> String {
        let custom = configuredShellPath.trimmingCharacters(in: .whitespacesAndNewlines)
        if !custom.isEmpty && FileManager.default.isExecutableFile(atPath: custom) {
            return custom
        }
        return ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
    }

    private func installCommandArrowMonitor() {
        keyEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
            guard let self else { return event }
            guard let window = self.terminalView.window else { return event }
            guard event.window === window else { return event }
            guard window.firstResponder === self.terminalView else { return event }

            guard let sequence = TerminalCommandArrowBehavior.sequence(
                for: event.modifierFlags,
                keyCode: event.keyCode,
                applicationCursor: self.terminalView.getTerminal().applicationCursor
            ) else {
                return event
            }

            self.terminalView.send(data: sequence[...])
            return nil
        }
    }

    func updateFontSize(_ size: CGFloat) {
        terminalView.font = NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
    }

    func applyTheme(_ theme: TerminalTheme) {
        // Keep the notch visually consistent while swapping the terminal's
        // default foreground color and ANSI palette for command output.
        terminalView.nativeBackgroundColor = backgroundColor
        terminalView.nativeForegroundColor = theme.foregroundColor
        terminalView.installColors(theme.ansiColors)
    }

    func terminate() {
        process?.terminate()
        process = nil
        isRunning = false
    }

    // MARK: - LocalProcessDelegate

    nonisolated func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
        Task { @MainActor in self.isRunning = false }
    }

    nonisolated func dataReceived(slice: ArraySlice<UInt8>) {
        let data = slice
        Task { @MainActor in self.terminalView.feed(byteArray: data) }
    }

    nonisolated func getWindowSize() -> winsize {
        var ws = winsize()
        ws.ws_col = 80
        ws.ws_row = 24
        return ws
    }

    // MARK: - TerminalViewDelegate

    func send(source: TerminalView, data: ArraySlice<UInt8>) {
        process?.send(data: data)
    }

    func setTerminalTitle(source: TerminalView, title: String) {
        self.title = title.isEmpty ? "shell" : title
    }

    func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
        guard newCols > 0, newRows > 0 else { return }
        guard let proc = process else { return }
        let fd = proc.childfd
        guard fd >= 0 else { return }

        var ws = winsize()
        ws.ws_col = UInt16(newCols)
        ws.ws_row = UInt16(newRows)
        _ = ioctl(fd, TIOCSWINSZ, &ws)
    }

    func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {
        guard let normalizedDirectory = Self.normalizedDirectory(directory) else { return }
        currentDirectory = normalizedDirectory
    }

    func scrolled(source: TerminalView, position: Double) {}
    func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}

    func clipboardCopy(source: TerminalView, content: Data) {
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setData(content, forType: .string)
    }

    func requestOpenLink(source: TerminalView, link: String, params: [String : String]) {
        if let url = URL(string: link) { NSWorkspace.shared.open(url) }
    }

    func bell(source: TerminalView) { NSSound.beep() }
    func iTermContent(source: TerminalView, content: ArraySlice<UInt8>) {}
}
