import AppKit
import SwiftTerm

// MARK: - Configuration
//
// Every value is read from the app bundle's Info.plist, so a single compiled
// binary can power many different .app bundles (each with its own command,
// working directory, title and icon). See make-app.sh.
//
//   MTCommand          String  Command to run, e.g. "claude". Empty = plain shell.
//   MTWorkingDirectory String  Directory to start in. "~" is expanded. Empty = home.
//   MTFontSize         Number  Font point size (default 13).
//   MTFontName         String  Font family name (default "Menlo"). Falls back to
//                              the system monospaced font if the name is missing.
//   MTWindowTitle      String  Window title (default = CFBundleName).
//
struct AppConfig {
    let command: String
    let workingDirectory: String?
    let fontSize: CGFloat
    let fontName: String
    let windowTitle: String

    static func load() -> AppConfig {
        let info = Bundle.main.infoDictionary ?? [:]

        let cmd = ((info["MTCommand"] as? String) ?? "")
            .trimmingCharacters(in: .whitespacesAndNewlines)

        var workdir: String? = nil
        if let raw = info["MTWorkingDirectory"] as? String,
           !raw.trimmingCharacters(in: .whitespaces).isEmpty {
            workdir = (raw as NSString).expandingTildeInPath
        }

        let size = (info["MTFontSize"] as? NSNumber)?.doubleValue ?? 13.0

        let fontName = ((info["MTFontName"] as? String) ?? "")
            .trimmingCharacters(in: .whitespacesAndNewlines)

        let title = (info["MTWindowTitle"] as? String)
            ?? (info["CFBundleName"] as? String)
            ?? "Terminal"

        return AppConfig(command: cmd,
                         workingDirectory: workdir,
                         fontSize: CGFloat(size),
                         fontName: fontName.isEmpty ? "Menlo" : fontName,
                         windowTitle: title)
    }
}

// MARK: - App delegate
final class AppDelegate: NSObject, NSApplicationDelegate, LocalProcessTerminalViewDelegate {
    private var window: NSWindow!
    private var terminalView: LocalProcessTerminalView!
    private var keyMonitor: Any?
    private let config = AppConfig.load()

    func applicationDidFinishLaunching(_ notification: Notification) {
        let frame = NSRect(x: 0, y: 0, width: 900, height: 560)

        terminalView = LocalProcessTerminalView(frame: frame)
        terminalView.processDelegate = self
        terminalView.font = resolvedFont()

        installKeyMonitor()

        window = NSWindow(
            contentRect: frame,
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false)
        window.title = config.windowTitle
        window.contentView = terminalView
        window.setFrameAutosaveName("MiniTermMain")
        window.center()
        window.makeKeyAndOrderFront(nil)
        window.makeFirstResponder(terminalView)

        setupMenu()
        launchProcess()
    }

    private func launchProcess() {
        let shell = userShell()

        // We always go through a LOGIN shell (-l) so PATH is set up exactly like
        // in a normal terminal (Homebrew, ~/.local/bin, etc.). Without this,
        // tools like `claude` are often "command not found".
        let args: [String]
        if config.command.isEmpty {
            args = ["-l"]
        } else {
            // Run the requested command, then hand control to an interactive
            // login shell so the window stays open after the command exits.
            args = ["-l", "-c", "\(config.command); exec \(shell) -l"]
        }

        let env = Terminal.getEnvironmentVariables(termName: "xterm-256color", trueColor: true)

        terminalView.startProcess(
            executable: shell,
            args: args,
            environment: env,
            execName: nil,
            currentDirectory: config.workingDirectory)
    }

    // Intercept Shift+Return (and Shift+Enter on the numpad) before it reaches
    // the terminal and send a line feed (\n) instead of the normal carriage
    // return (\r). This lets CLIs like Claude Code insert a newline in
    // multi-line input while plain Enter still submits. We use a local event
    // monitor because SwiftTerm's keyDown(with:) is `public`, not `open`, so it
    // can't be overridden from outside the module.
    private func installKeyMonitor() {
        keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
            guard let self = self else { return event }
            // Only act when our window is focused and the keyboard focus is in
            // the terminal (the view itself or one of its subviews).
            guard self.window.isKeyWindow else { return event }
            if let responder = self.window.firstResponder as? NSView,
               !(responder === self.terminalView || responder.isDescendant(of: self.terminalView)) {
                return event
            }

            let isReturn = (event.keyCode == 36 || event.keyCode == 76)
            let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
            if isReturn && mods == .shift {
                self.terminalView.send(txt: "\n")
                return nil   // consume the event
            }
            return event
        }
    }

    // Resolve the configured font, falling back gracefully so the app never
    // launches with a broken/empty font if the name doesn't exist on the system.
    private func resolvedFont() -> NSFont {
        if let f = NSFont(name: config.fontName, size: config.fontSize) {
            return f
        }
        if let menlo = NSFont(name: "Menlo", size: config.fontSize) {
            return menlo
        }
        return NSFont.monospacedSystemFont(ofSize: config.fontSize, weight: .regular)
    }

    private func userShell() -> String {
        if let shell = ProcessInfo.processInfo.environment["SHELL"], !shell.isEmpty {
            return shell
        }
        return "/bin/zsh"
    }

    private func setupMenu() {
        let mainMenu = NSMenu()

        // Application menu
        let appItem = NSMenuItem()
        mainMenu.addItem(appItem)
        let appMenu = NSMenu()
        let name = config.windowTitle
        appMenu.addItem(withTitle: "Hide \(name)",
                        action: #selector(NSApplication.hide(_:)),
                        keyEquivalent: "h")
        appMenu.addItem(NSMenuItem.separator())
        appMenu.addItem(withTitle: "Quit \(name)",
                        action: #selector(NSApplication.terminate(_:)),
                        keyEquivalent: "q")
        appItem.submenu = appMenu

        // Edit menu (enables ⌘C / ⌘V in the terminal)
        let editItem = NSMenuItem()
        mainMenu.addItem(editItem)
        let editMenu = NSMenu(title: "Edit")
        editMenu.addItem(withTitle: "Copy",
                         action: #selector(NSText.copy(_:)),
                         keyEquivalent: "c")
        editMenu.addItem(withTitle: "Paste",
                         action: #selector(NSText.paste(_:)),
                         keyEquivalent: "v")
        editItem.submenu = editMenu

        NSApp.mainMenu = mainMenu
    }

    // MARK: LocalProcessTerminalViewDelegate
    func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {}

    func setTerminalTitle(source: LocalProcessTerminalView, title: String) {
        // Keep a stable window title. Uncomment to follow the shell's title:
        // window.title = title.isEmpty ? config.windowTitle : title
    }

    func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {}

    func processTerminated(source: TerminalView, exitCode: Int32?) {
        NSApp.terminate(nil)
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
}

// MARK: - Entry point
let app = NSApplication.shared
app.setActivationPolicy(.regular)   // a real Dock app, not a background process
let delegate = AppDelegate()
app.delegate = delegate
app.activate(ignoringOtherApps: true)
app.run()
