//
//  NativeTextEditor.swift
//  Confab
//
//  NSViewRepresentable text editor with correct Enter-key semantics:
//  plain Enter → send, Shift+Enter or Cmd+Enter → newline.
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen

import SwiftUI
import AppKit

struct NativeTextEditor: NSViewRepresentable {
    @Binding var text: String
    var font: NSFont
    var textColor: NSColor
    var isFocused: Bool

    /// Plain Enter (no modifiers). Return true if the event was consumed.
    var onReturn: () -> Bool
    /// Escape key. Return true if consumed.
    var onEscape: () -> Bool
    /// Up arrow. Return true if consumed.
    var onUpArrow: () -> Bool
    /// Down arrow. Return true if consumed.
    var onDownArrow: () -> Bool
    /// Called when the view gains or loses first-responder status.
    var onFocusChange: (Bool) -> Void

    // MARK: - NSViewRepresentable

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSScrollView()
        scrollView.hasVerticalScroller = false
        scrollView.hasHorizontalScroller = false
        scrollView.drawsBackground = false
        scrollView.borderType = .noBorder

        let tv = context.coordinator.textView
        tv.delegate = context.coordinator
        tv.isEditable = true
        tv.isRichText = false
        tv.drawsBackground = false
        tv.backgroundColor = .clear
        tv.isAutomaticQuoteSubstitutionEnabled = false
        tv.isAutomaticDashSubstitutionEnabled = false
        tv.isAutomaticSpellingCorrectionEnabled = true
        tv.isContinuousSpellCheckingEnabled = true
        tv.allowsUndo = true
        tv.isVerticallyResizable = true
        tv.isHorizontallyResizable = false
        tv.autoresizingMask = [.width]
        tv.textContainer?.widthTracksTextView = true
        tv.textContainerInset = NSSize(width: 8, height: 6)

        scrollView.documentView = tv
        return scrollView
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        let tv = context.coordinator.textView
        let coord = context.coordinator

        // Update text only when it differs (avoids caret-jumping on every keystroke)
        if tv.string != text {
            let sel = tv.selectedRanges
            tv.string = text
            let len = (tv.string as NSString).length
            tv.selectedRanges = sel.map { v in
                let r = v.rangeValue
                let loc = min(r.location, len)
                let length = min(r.length, max(0, len - loc))
                return NSValue(range: NSRange(location: loc, length: length))
            }
        }

        if tv.font != font       { tv.font = font }
        if tv.textColor != textColor { tv.textColor = textColor }

        // Keep coordinator callbacks current with each SwiftUI render
        coord.textBinding  = $text
        coord.onReturn     = onReturn
        coord.onEscape     = onEscape
        coord.onUpArrow    = onUpArrow
        coord.onDownArrow  = onDownArrow
        coord.onFocusChange = onFocusChange
        coord.baseFont      = font
        coord.baseTextColor = textColor

        coord.applyInlineCodeStyling()

        if isFocused {
            DispatchQueue.main.async {
                guard let window = tv.window, window.firstResponder !== tv else { return }
                window.makeFirstResponder(tv)
            }
        }
    }

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

    /// Ranges of complete, closed single-backtick spans on one line (e.g. "Hello `code` world" →
    /// the range covering `` `code` ``, backticks included). An unterminated backtick with no
    /// closing pair yet is deliberately not matched — it only lights up once closed. Doesn't match
    /// across a newline, so a fenced block's opening/closing ``` triples never get mistaken for
    /// this. Pulled out as a pure function so it's testable without a live NSTextView.
    nonisolated static func inlineCodeRanges(in text: String) -> [NSRange] {
        guard let regex = try? NSRegularExpression(pattern: "`[^`\\n]+`") else { return [] }
        let nsText = text as NSString
        return regex.matches(in: text, range: NSRange(location: 0, length: nsText.length)).map { $0.range }
    }

    /// Complete, closed fenced blocks, each with the full ```…``` range, the language tag (if any,
    /// from right after the opening fence, e.g. "```python"), and the range of just the code
    /// content (excluding the fences and the language-tag line). An unterminated fence with no
    /// closing ``` yet is not matched, same "only when closed" rule as inline spans.
    nonisolated static func fencedCodeBlocks(in text: String) -> [(fullRange: NSRange, language: String?, codeRange: NSRange)] {
        guard let regex = try? NSRegularExpression(pattern: "```([A-Za-z0-9_+-]*)[ \\t]*\\n([\\s\\S]*?)```") else { return [] }
        let nsText = text as NSString
        let matches = regex.matches(in: text, range: NSRange(location: 0, length: nsText.length))
        return matches.map { match in
            let langRange = match.range(at: 1)
            let language = langRange.length > 0 ? nsText.substring(with: langRange) : nil
            return (fullRange: match.range, language: language, codeRange: match.range(at: 2))
        }
    }

    /// Ranges of complete, closed triple-backtick fenced blocks (may span multiple lines,
    /// including an optional language tag right after the opening fence). An unterminated fence
    /// with no closing ``` yet is not matched, same "only when closed" rule as inline spans.
    nonisolated static func fencedCodeBlockRanges(in text: String) -> [NSRange] {
        fencedCodeBlocks(in: text).map { $0.fullRange }
    }

    /// Every range that should render as code: fenced ```blocks``` plus inline `spans` — except
    /// any inline span that falls inside a fenced block (so a stray backtick inside a code block's
    /// own content never gets double-styled or splits the block's styling).
    nonisolated static func codeStyledRanges(in text: String) -> [NSRange] {
        let fenced = fencedCodeBlockRanges(in: text)
        let inline = inlineCodeRanges(in: text).filter { inlineRange in
            !fenced.contains { NSIntersectionRange($0, inlineRange).length > 0 }
        }
        return (fenced + inline).sorted { $0.location < $1.location }
    }

    // MARK: - Coordinator

    final class Coordinator: NSObject, NSTextViewDelegate {
        let textView = KeyableNSTextView()

        // Updated on every SwiftUI render via updateNSView
        var textBinding:   Binding<String>?
        var onReturn:      () -> Bool = { false }
        var onEscape:      () -> Bool = { false }
        var onUpArrow:     () -> Bool = { false }
        var onDownArrow:   () -> Bool = { false }
        var onFocusChange: (Bool) -> Void = { _ in }
        var baseFont:      NSFont = .systemFont(ofSize: NSFont.systemFontSize)
        var baseTextColor: NSColor = .textColor

        override init() {
            super.init()
            textView.coordinator = self
        }

        func textDidChange(_ notification: Notification) {
            guard let tv = notification.object as? NSTextView else { return }
            textBinding?.wrappedValue = tv.string
            applyInlineCodeStyling()
        }

        /// Re-applies code styling (inline spans and fenced blocks) to the whole text after any
        /// edit — purely visual (font/color attributes on the existing characters), never touches
        /// the actual string content, so the backticks/fences stay in the sent message as typed.
        func applyInlineCodeStyling() {
            let storage = textView.textStorage!
            let fullRange = NSRange(location: 0, length: storage.length)
            let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)

            storage.beginEditing()
            storage.setAttributes([.font: baseFont, .foregroundColor: baseTextColor], range: fullRange)
            for range in NativeTextEditor.codeStyledRanges(in: storage.string) {
                storage.addAttributes([
                    .font: monoFont,
                    .backgroundColor: NSColor.textColor.withAlphaComponent(0.08)
                ], range: range)
            }
            applySyntaxHighlighting(to: storage)
            storage.endEditing()
        }

        /// Colors keywords/strings/comments/numbers inside each fenced block's code content using
        /// the same per-language `SyntaxHighlighter` already used to render assistant messages —
        /// only overlays `.foregroundColor` on top of the monospace/background pass above, so it
        /// never fights that pass's font.
        private func applySyntaxHighlighting(to storage: NSTextStorage) {
            let nsText = storage.string as NSString
            for block in NativeTextEditor.fencedCodeBlocks(in: storage.string) {
                let codeRange = block.codeRange
                guard codeRange.location != NSNotFound, codeRange.length > 0 else { continue }
                let code = nsText.substring(with: codeRange)
                let highlighted = SyntaxHighlighter.highlight(code: code, language: block.language)
                // Read runs directly off the AttributedString rather than bridging to
                // NSAttributedString — that bridge stores SwiftUI's `.foregroundColor` under a
                // private `SwiftUI.ForegroundColor` key, not the standard Cocoa `.foregroundColor`
                // key, so it never actually carries the color over (confirmed empirically).
                for run in highlighted.runs {
                    guard let color = run.foregroundColor else { continue }
                    let runNSRange = NSRange(run.range, in: highlighted)
                    let absoluteRange = NSRange(location: codeRange.location + runNSRange.location, length: runNSRange.length)
                    storage.addAttribute(.foregroundColor, value: NSColor(color), range: absoluteRange)
                }
            }
        }
    }
}

// MARK: - KeyableNSTextView

/// NSTextView that routes Return / Escape / arrow keys to the SwiftUI
/// coordinator before the AppKit default handling runs.
final class KeyableNSTextView: NSTextView {
    weak var coordinator: NativeTextEditor.Coordinator?

    override func keyDown(with event: NSEvent) {
        guard let coord = coordinator else { super.keyDown(with: event); return }

        let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
        let shift = flags.contains(.shift)
        let cmd   = flags.contains(.command)

        switch event.keyCode {
        case 36: // Return
            if shift || cmd {
                // Shift+Enter or Cmd+Enter → literal newline
                insertNewlineIgnoringFieldEditor(nil)
            } else {
                // Plain Enter → let SwiftUI decide (send or select dropdown item)
                if !coord.onReturn() {
                    insertNewlineIgnoringFieldEditor(nil)
                }
            }
        case 53: // Escape
            if !coord.onEscape() { super.keyDown(with: event) }
        case 126: // Up arrow
            if !coord.onUpArrow() { super.keyDown(with: event) }
        case 125: // Down arrow
            if !coord.onDownArrow() { super.keyDown(with: event) }
        default:
            super.keyDown(with: event)
        }
    }

    override func becomeFirstResponder() -> Bool {
        let ok = super.becomeFirstResponder()
        if ok { coordinator?.onFocusChange(true) }
        return ok
    }

    override func resignFirstResponder() -> Bool {
        let ok = super.resignFirstResponder()
        if ok { coordinator?.onFocusChange(false) }
        return ok
    }
}
