import SwiftUI
import AppKit

/// Which keystroke sends a message from the chat composer. Persisted in
/// `UserDefaults` and surfaced in Settings; both the home chat bar and the session
/// composer read it so the behavior — and the send-button glyph — stay consistent.
enum SubmitKeyMode: String, CaseIterable, Identifiable {
    /// Return sends the message; Shift+Return inserts a newline. (⌘Return also sends.)
    case returnSends
    /// Return and Shift+Return both insert a newline; only ⌘Return sends.
    case modifierSends

    var id: String { rawValue }
    static let storageKey = "nucleic.composer.submitKey"

    var label: String {
        switch self {
        case .returnSends: "Return"
        case .modifierSends: "⌘ + Return"
        }
    }

    var detail: String {
        switch self {
        case .returnSends: "Press Return to send. Shift+Return adds a new line."
        case .modifierSends: "Press ⌘Return to send. Return and Shift+Return add a new line."
        }
    }
}

/// Small glyph mirroring the keystroke that sends the message — the Return symbol on
/// its own, or ⌘ + Return — so the send button shows exactly which key fires it.
struct SubmitKeyIcon: View {
    let mode: SubmitKeyMode

    var body: some View {
        switch mode {
        case .returnSends:
            Image(systemName: "return")
                .font(.system(size: 16, weight: .semibold))
        case .modifierSends:
            HStack(spacing: 1) {
                Image(systemName: "command")
                Image(systemName: "return")
            }
            .font(.system(size: 12, weight: .semibold))
        }
    }
}

/// A multiline chat input backed by `NSTextView` so the composer can grow with the
/// text, scroll once it reaches its max height, and distinguish Return /
/// Shift+Return / ⌘Return for sending vs. inserting a newline — none of which the
/// SwiftUI `TextField` exposes on macOS. Visual chrome (padding, background) is
/// applied by the caller; this view draws only the text and placeholder.
struct ChatInputField: NSViewRepresentable {
    @Binding var text: String
    var placeholder: String
    var submitMode: SubmitKeyMode
    var isEnabled: Bool
    /// The fitted height (one line at rest, growing with the text, capped at half the
    /// window). The caller applies it via `.frame(height:)`; past the cap the text
    /// view scrolls inside that frame. A binding rather than `intrinsicContentSize`
    /// because SwiftUI lets an unconstrained `NSScrollView` grow to fill its parent.
    @Binding var height: CGFloat
    var onSend: () -> Void
    /// Files/images pasted into or dropped onto the field, handed up so the composer can stage
    /// them as attachment chips. Defaulted so callers that don't accept attachments are unaffected.
    var onAttach: ([ComposerAttachment]) -> Void = { _ in }
    /// Fires true while a file/image drag hovers the field and false when it leaves or drops, so
    /// the composer can light up the drop zone.
    var onDropTargetedChanged: (Bool) -> Void = { _ in }

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    /// Resting height when no window is available yet — one line plus insets, so the
    /// composer starts at its prior single-line size instead of flashing tall/short.
    static let restingHeight: CGFloat = 24

    func makeNSView(context: Context) -> HeightTrackingScrollView {
        let textView = ComposerTextView()
        textView.delegate = context.coordinator
        textView.isRichText = false
        textView.allowsUndo = true
        textView.font = .systemFont(ofSize: NSFont.systemFontSize)
        textView.textColor = .labelColor
        textView.insertionPointColor = .labelColor
        textView.drawsBackground = false
        textView.backgroundColor = .clear
        textView.textContainerInset = NSSize(width: 0, height: 2)
        textView.isVerticallyResizable = true
        textView.isHorizontallyResizable = false
        textView.autoresizingMask = [.width]
        textView.minSize = NSSize(width: 0, height: 0)
        textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        textView.textContainer?.widthTracksTextView = true
        textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
        textView.textContainer?.lineFragmentPadding = 0
        textView.string = text
        textView.placeholderString = placeholder
        // Accept file/image drags in addition to the text view's own drag types (don't replace
        // them — that would break dragging text into the field). Whichever type fires, the drop
        // handler re-reads the pasteboard for files or images.
        let mediaTypes: [NSPasteboard.PasteboardType] = [
            .fileURL, .png, .tiff, .pdf, NSPasteboard.PasteboardType("public.jpeg"),
        ]
        textView.registerForDraggedTypes(textView.registeredDraggedTypes + mediaTypes)

        let scroll = HeightTrackingScrollView()
        scroll.documentView = textView
        scroll.drawsBackground = false
        scroll.borderType = .noBorder
        scroll.hasVerticalScroller = true
        scroll.hasHorizontalScroller = false
        scroll.autohidesScrollers = true

        context.coordinator.textView = textView
        context.coordinator.scrollView = scroll
        scroll.onLayout = { [weak coordinator = context.coordinator] in
            coordinator?.recomputeHeight()
        }
        apply(to: textView, coordinator: context.coordinator)
        context.coordinator.recomputeHeight()
        return scroll
    }

    func updateNSView(_ scroll: HeightTrackingScrollView, context: Context) {
        context.coordinator.parent = self
        guard let textView = scroll.documentView as? ComposerTextView else { return }
        if textView.string != text { textView.string = text }
        apply(to: textView, coordinator: context.coordinator)
        context.coordinator.recomputeHeight()
    }

    /// Push the current SwiftUI-side configuration onto the text view. Called from
    /// both make/update so the two paths can't drift.
    private func apply(to textView: ComposerTextView, coordinator: Coordinator) {
        textView.placeholderString = placeholder
        textView.isEditable = isEnabled
        textView.isSelectable = isEnabled
        textView.alphaValue = isEnabled ? 1 : 0.5
        textView.submitMode = submitMode
        textView.onSend = { [weak textView, weak coordinator] in
            guard let textView, textView.isEditable else { return }
            coordinator?.parent.onSend()
        }
        // Route through the coordinator's live `parent` so these never capture a stale closure.
        textView.onAttach = { [weak coordinator] attachments in
            coordinator?.parent.onAttach(attachments)
        }
        textView.onDropTargetedChanged = { [weak coordinator] targeted in
            coordinator?.parent.onDropTargetedChanged(targeted)
        }
    }

    @MainActor
    final class Coordinator: NSObject, NSTextViewDelegate {
        var parent: ChatInputField
        weak var textView: ComposerTextView?
        weak var scrollView: HeightTrackingScrollView?

        init(_ parent: ChatInputField) { self.parent = parent }

        func textDidChange(_ notification: Notification) {
            guard let textView else { return }
            parent.text = textView.string
            recomputeHeight()
        }

        /// Report the fitted height: one line at rest, growing with the text, capped
        /// at half the window's height (beyond which the text view scrolls).
        func recomputeHeight() {
            guard let textView,
                  let layoutManager = textView.layoutManager,
                  let container = textView.textContainer else { return }
            layoutManager.ensureLayout(for: container)
            let font = textView.font ?? .systemFont(ofSize: NSFont.systemFontSize)
            let lineHeight = layoutManager.defaultLineHeight(for: font)
            let inset = textView.textContainerInset.height * 2
            let content = layoutManager.usedRect(for: container).height + inset
            let minH = lineHeight + inset
            // No taller than half the window; fall back to ~6 lines before it's onscreen.
            let windowHalf = textView.window.map { $0.contentLayoutRect.height / 2 }
            let maxH = max(minH, windowHalf ?? (lineHeight * 6 + inset))
            let clamped = min(max(content, minH), maxH)
            guard abs(parent.height - clamped) > 0.5 else { return }
            // Defer the binding write: recompute can fire from inside a SwiftUI
            // update (updateNSView) or AppKit layout pass.
            DispatchQueue.main.async { [weak self] in
                self?.parent.height = clamped
            }
        }
    }
}

/// Scroll view that notifies on relayout (e.g. width changes from a window resize)
/// so the fitted height can be recomputed for the new text wrapping.
final class HeightTrackingScrollView: NSScrollView {
    var onLayout: (() -> Void)?

    override func layout() {
        super.layout()
        onLayout?()
    }
}

/// Text view that routes Return/Shift+Return/⌘Return to send-or-newline per the
/// active `SubmitKeyMode`, and paints a placeholder when empty.
final class ComposerTextView: NSTextView {
    var placeholderString: String = "" { didSet { needsDisplay = true } }
    var submitMode: SubmitKeyMode = .modifierSends
    var onSend: (() -> Void)?
    /// Files/images pasted or dropped onto the field. Set by `ChatInputField`.
    var onAttach: (([ComposerAttachment]) -> Void)?
    /// Drag-hover state for the field's drop-zone highlight.
    var onDropTargetedChanged: ((Bool) -> Void)?

    // MARK: Paste

    /// Intercept ⌘V when the clipboard holds files or an image, staging them as attachments
    /// instead of pasting a path/picture into the text. Ordinary text (including rich text that
    /// merely carries an image rep) falls through to the normal paste.
    override func paste(_ sender: Any?) {
        let pasteboard = NSPasteboard.general
        if let onAttach, ComposerAttachment.pasteboardHasAttachments(pasteboard) {
            let attachments = ComposerAttachment.fromPasteboard(pasteboard)
            if !attachments.isEmpty {
                onAttach(attachments)
                return
            }
        }
        super.paste(sender)
    }

    // MARK: Drag & drop

    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        if ComposerAttachment.pasteboardHasDraggableMedia(sender.draggingPasteboard) {
            onDropTargetedChanged?(true)
            return .copy
        }
        return super.draggingEntered(sender)
    }

    override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
        if ComposerAttachment.pasteboardHasDraggableMedia(sender.draggingPasteboard) {
            return .copy
        }
        return super.draggingUpdated(sender)
    }

    override func draggingExited(_ sender: NSDraggingInfo?) {
        onDropTargetedChanged?(false)
        super.draggingExited(sender)
    }

    override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
        if ComposerAttachment.pasteboardHasDraggableMedia(sender.draggingPasteboard) { return true }
        return super.prepareForDragOperation(sender)
    }

    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        onDropTargetedChanged?(false)
        let pasteboard = sender.draggingPasteboard
        if let onAttach, ComposerAttachment.pasteboardHasDraggableMedia(pasteboard) {
            let attachments = ComposerAttachment.fromPasteboard(pasteboard)
            if !attachments.isEmpty {
                onAttach(attachments)
                return true
            }
        }
        return super.performDragOperation(sender)
    }

    override func keyDown(with event: NSEvent) {
        let isReturn = event.keyCode == 36 || event.keyCode == 76  // Return, numeric-keypad Enter
        // Don't hijack Return while an input method is composing (e.g. confirming
        // a Pinyin/Kana candidate), and never send from a disabled composer.
        if isReturn, isEditable, !hasMarkedText() {
            let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
            let shift = flags.contains(.shift)
            let command = flags.contains(.command)
            let send: Bool
            switch submitMode {
            case .returnSends: send = command || !shift   // Return / ⌘Return send; Shift+Return = newline
            case .modifierSends: send = command           // only ⌘Return sends
            }
            if send {
                onSend?()
                return
            }
            // Otherwise fall through so AppKit inserts the newline.
        }
        super.keyDown(with: event)
    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        guard string.isEmpty, !placeholderString.isEmpty else { return }
        let attrs: [NSAttributedString.Key: Any] = [
            .foregroundColor: NSColor.placeholderTextColor,
            .font: font ?? .systemFont(ofSize: NSFont.systemFontSize),
        ]
        let padding = textContainer?.lineFragmentPadding ?? 0
        let point = NSPoint(x: textContainerInset.width + padding, y: textContainerInset.height)
        placeholderString.draw(at: point, withAttributes: attrs)
    }
}
