import SwiftUI
import UIKit

// MARK: - KeyboardTrackingView

/// A zero-height UIView used as `inputAccessoryView`.
/// KVO on `superview.center` gives pixel-perfect keyboard position
/// during interactive dismiss — the most reliable path for composer sync.
final class KeyboardTrackingView: UIView {

    var onHeightChange: ((CGFloat) -> Void)?

    private var observation: NSKeyValueObservation?

    override init(frame: CGRect) {
        super.init(frame: .init(x: 0, y: 0, width: frame.width, height: 0))
        autoresizingMask = .flexibleWidth
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError() }

    override var intrinsicContentSize: CGSize {
        .init(width: UIView.noIntrinsicMetric, height: 0)
    }

    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        observation?.invalidate()
        observation = nil

        guard let sv = superview else { return }

        // Only observe .center — .bounds fires simultaneously for the same
        // position change, doubling KVO callbacks with no new information.
        observation = sv.observe(\.center, options: [.new]) { [weak self] view, _ in
            self?.reportHeight(from: view)
        }
    }

    private var lastReportedHeight: CGFloat = -1

    private func reportHeight(from hostView: UIView) {
        guard let window = hostView.window else { return }
        let screenHeight = window.screen.bounds.height
        let hostFrame = hostView.convert(hostView.bounds, to: nil)
        let keyboardHeight = max(0, screenHeight - hostFrame.origin.y)
        // Throttle: only fire callback when rounded height actually changes.
        // Sub-point changes are invisible but still trigger full SwiftUI layout.
        // This halves the number of body evaluations during interactive dismiss.
        let rounded = round(keyboardHeight)
        guard abs(rounded - lastReportedHeight) > 1 else { return }
        lastReportedHeight = rounded
        onHeightChange?(rounded)
    }

    deinit {
        observation?.invalidate()
    }
}

// MARK: - ChatInputTextView

/// UITextView subclass with a placeholder label that stays visible while empty.
final class ChatInputTextView: UITextView {

    let trackingView = KeyboardTrackingView(
        frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0)
    )

    /// Placeholder label — visible when text is empty, even while focused.
    let placeholderLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.isUserInteractionEnabled = false
        return label
    }()

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        // KVO-based tracking for interactive keyboard dismiss.
        // trackingView is zero-height — superview.center KVO gives
        // pixel-perfect keyboard position during swipe-to-dismiss.
        inputAccessoryView = trackingView
        inputAssistantItem.leadingBarButtonGroups = []
        inputAssistantItem.trailingBarButtonGroups = []
        addSubview(placeholderLabel)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // Position placeholder to match text position exactly
        let insets = textContainerInset
        let padding = textContainer.lineFragmentPadding
        placeholderLabel.frame.origin = CGPoint(x: insets.left + padding, y: insets.top)
        placeholderLabel.frame.size = CGSize(
            width: bounds.width - insets.left - insets.right - padding * 2,
            height: placeholderLabel.intrinsicContentSize.height
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError() }
}

// MARK: - ChatTextInput (UIViewRepresentable)

struct ChatTextInput: UIViewRepresentable {

    @Binding var text: String
    @Binding var isFocused: Bool
    var onKeyboardHeightChange: (CGFloat) -> Void
    var onUserTextInsertion: () -> Void = {}
    var onMultilineChange: (Bool) -> Void = { _ in }
    /// Reports the ideal height (clamped to max 5 lines) on every text change.
    /// Used to drive `@State` frame height in the composer layout.
    var onTextHeightChange: (CGFloat) -> Void = { _ in }
    var font: UIFont = .systemFont(ofSize: 17, weight: .regular)
    var textColor: UIColor = .white
    var placeholderColor: UIColor = UIColor.white.withAlphaComponent(0.35)
    var placeholder: String = "Message"

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

    func makeUIView(context: Context) -> ChatInputTextView {
        let tv = ChatInputTextView()
        tv.delegate = context.coordinator
        tv.font = font
        tv.textColor = textColor
        tv.backgroundColor = .clear
        tv.tintColor = UIColor(RosettaColors.primaryBlue)
        tv.isScrollEnabled = false
        tv.textContainerInset = UIEdgeInsets(top: 7, left: 2, bottom: 7, right: 0)
        tv.textContainer.lineFragmentPadding = 0
        tv.autocapitalizationType = .sentences
        tv.autocorrectionType = .default
        tv.keyboardAppearance = .dark
        tv.returnKeyType = .default

        // Placeholder label (stays visible when text is empty, even if focused)
        tv.placeholderLabel.text = placeholder
        tv.placeholderLabel.font = font
        tv.placeholderLabel.textColor = placeholderColor
        tv.placeholderLabel.isHidden = !text.isEmpty

        tv.trackingView.onHeightChange = { [weak coordinator = context.coordinator] height in
            coordinator?.handleKeyboardHeight(height)
        }

        // Set initial text
        if !text.isEmpty {
            tv.text = text
        }

        return tv
    }

    func updateUIView(_ tv: ChatInputTextView, context: Context) {
        let coord = context.coordinator
        coord.parent = self

        // Sync text from SwiftUI → UIKit (avoid loop)
        if !coord.isUpdatingText {
            if text != tv.text {
                tv.text = text
                coord.invalidateHeight(tv)
            }
            tv.placeholderLabel.isHidden = !text.isEmpty
        }

        // Sync focus without replaying stale async requests during interactive dismiss.
        coord.syncFocus(for: tv)
    }

    func sizeThatFits(_ proposal: ProposedViewSize, uiView tv: ChatInputTextView, context: Context) -> CGSize? {
        let maxWidth = proposal.width ?? UIScreen.main.bounds.width
        let lineHeight = font.lineHeight
        let maxLines: CGFloat = 5
        let insets = tv.textContainerInset
        let maxTextHeight = lineHeight * maxLines
        let maxTotalHeight = maxTextHeight + insets.top + insets.bottom

        let fittingSize = tv.sizeThatFits(CGSize(width: maxWidth, height: .greatestFiniteMagnitude))
        let clampedHeight = min(fittingSize.height, maxTotalHeight)

        // Enable scrolling when content exceeds max height
        let shouldScroll = fittingSize.height > maxTotalHeight
        if tv.isScrollEnabled != shouldScroll {
            tv.isScrollEnabled = shouldScroll
        }

        return CGSize(width: maxWidth, height: max(36, clampedHeight))
    }

    // MARK: - Coordinator

    final class Coordinator: NSObject, UITextViewDelegate {
        var parent: ChatTextInput
        var isUpdatingText = false
        private var wasMultiline = false
        private var pendingFocusSync: DispatchWorkItem?

        init(parent: ChatTextInput) {
            self.parent = parent
        }

        func handleKeyboardHeight(_ height: CGFloat) {
            parent.onKeyboardHeightChange(height)
        }

        // MARK: UITextViewDelegate

        func textViewDidBeginEditing(_ tv: UITextView) {
            pendingFocusSync?.cancel()
            // Placeholder stays visible — only hidden when user types
            if !parent.isFocused {
                parent.isFocused = true
            }
        }

        func textViewDidEndEditing(_ tv: UITextView) {
            pendingFocusSync?.cancel()
            // Must be synchronous — async causes race condition with .ignoresSafeArea(.keyboard):
            // padding animation triggers updateUIView before isFocused is updated,
            // causing becomeFirstResponder() → keyboard reopens.
            if parent.isFocused {
                parent.isFocused = false
            }
        }

        func textView(
            _ tv: UITextView,
            shouldChangeTextIn range: NSRange,
            replacementText text: String
        ) -> Bool {
            guard !text.isEmpty, text != "\n" else { return true }
            parent.onUserTextInsertion()
            return true
        }

        func textViewDidChange(_ tv: UITextView) {
            isUpdatingText = true
            parent.text = tv.text ?? ""
            isUpdatingText = false
            // Toggle placeholder based on content
            if let chatTV = tv as? ChatInputTextView {
                chatTV.placeholderLabel.isHidden = !tv.text.isEmpty
            }
            invalidateHeight(tv)
        }

        func invalidateHeight(_ tv: UITextView) {
            tv.invalidateIntrinsicContentSize()
            // Force contentSize recalculation before checking multiline.
            // Without this, contentSize still reflects the OLD text after deletion.
            tv.setNeedsLayout()
            tv.layoutIfNeeded()
            checkMultiline(tv)
            reportHeight(tv)
        }

        /// Computes the ideal height (clamped to max 5 lines) and reports to SwiftUI.
        /// This drives `@State textInputHeight` in the composer → forces relayout.
        private func reportHeight(_ tv: UITextView) {
            let lineHeight = tv.font?.lineHeight ?? 20
            let maxLines: CGFloat = 5
            let insets = tv.textContainerInset
            let maxTextHeight = lineHeight * maxLines
            let maxTotalHeight = maxTextHeight + insets.top + insets.bottom
            let fittingSize = tv.sizeThatFits(
                CGSize(width: tv.bounds.width, height: .greatestFiniteMagnitude)
            )
            let height = max(36, min(fittingSize.height, maxTotalHeight))
            parent.onTextHeightChange(height)
        }

        private func checkMultiline(_ tv: UITextView) {
            let lineHeight = tv.font?.lineHeight ?? 20
            let singleLineHeight = lineHeight + tv.textContainerInset.top + tv.textContainerInset.bottom
            let threshold = singleLineHeight + lineHeight * 0.5
            // Use sizeThatFits instead of contentSize — contentSize reflects the current
            // FRAME (still multiline-sized), not the actual text height. sizeThatFits
            // calculates the needed height for the text, independent of the current frame.
            let fittingHeight = tv.sizeThatFits(CGSize(width: tv.bounds.width, height: .greatestFiniteMagnitude)).height
            let isMultiline = fittingHeight > threshold
            if isMultiline != wasMultiline {
                wasMultiline = isMultiline
                parent.onMultilineChange(isMultiline)
            }
        }

        func syncFocus(for tv: UITextView) {
            pendingFocusSync?.cancel()

            let wantsFocus = parent.isFocused
            guard wantsFocus != tv.isFirstResponder else { return }

            let workItem = DispatchWorkItem { [weak self, weak tv] in
                guard let self, let tv else { return }
                guard self.parent.isFocused == wantsFocus else { return }

                if wantsFocus {
                    guard !tv.isFirstResponder else { return }
                    tv.becomeFirstResponder()
                } else {
                    guard tv.isFirstResponder else { return }
                    tv.resignFirstResponder()
                }
            }

            pendingFocusSync = workItem
            DispatchQueue.main.async(execute: workItem)
        }
    }
}
