import SwiftUI
import Combine
import UIKit

/// Drives keyboard-related positioning for the chat composer.
///
/// Published property:
/// - `keyboardPadding`: bottom padding to apply when keyboard is visible
///
/// Animation strategy:
/// - Notification (show/hide): `spacerPadding` driven by CADisplayLink with curve-predicted
///   bezier evaluation. SHOW uses 2.5-frame advance to stay ahead of UIKit composer
///   (prevents message overlap). HIDE uses 1-frame advance. `keyboardPadding`
///   driven by sync view reading (for ComposerHostView consumers).
/// - KVO (interactive dismiss): raw assignment at 30fps via coalescing.
@MainActor
final class KeyboardTracker: ObservableObject {

    static let shared = KeyboardTracker()

    /// Bottom padding — updated incrementally at display refresh rate.
    /// Used by `KeyboardPaddedView` (composer offset) for smooth animation.
    @Published private(set) var keyboardPadding: CGFloat = 0

    /// Target padding — jumps to final value in 1 step.
    /// Used by `KeyboardSpacer` (scroll content) to avoid 16 VStack re-layouts
    /// per animation. Messages are scroll-anchored, so the jump is invisible.
    @Published private(set) var spacerPadding: CGFloat = 0

    private var isAnimating = false
    /// Public flag for BubbleContextMenuOverlay to skip updateUIView during animation.
    /// NOT @Published — read directly from UIViewRepresentable, no observation.
    private(set) var isAnimatingKeyboard = false
    private let bottomInset: CGFloat
    private var pendingResetTask: Task<Void, Never>?
    private var spacerUpdateTask: Task<Void, Never>?
    private var cancellables = Set<AnyCancellable>()
    private var lastNotificationPadding: CGFloat = 0

    /// UIKit composer container — animated in same CA transaction as keyboard.
    /// Set by ComposerUIKitContainer, cleared on dismantle.
    weak var composerHostView: ComposerHostView?

    // CADisplayLink-based animation state (notification-driven show/hide)
    private var displayLinkProxy: DisplayLinkProxy?
    private var animStartPadding: CGFloat = 0
    private var animTargetPadding: CGFloat = 0
    private var animStartTime: CFTimeInterval = 0
    private var animDuration: CFTimeInterval = 0.25
    private var animTickCount = 0
    private var animationNumber = 0
    private var lastTickTime: CFTimeInterval = 0
    /// Monotonic guard — sync view can give non-monotonic eased values
    /// in the first 2 ticks while Core Animation commits the animation.
    private var lastEased: CGFloat = 0
    /// Previous tick's monotonic eased — for velocity prediction.
    private var previousMonotonicEased: CGFloat = 0

    // Cubic bezier control points — deterministic keyboard curve approximation.
    private var bezierP1x: CGFloat = 0.25
    private var bezierP1y: CGFloat = 0.1
    private var bezierP2x: CGFloat = 0.25
    private var bezierP2y: CGFloat = 1.0

    // KVO coalescing — buffers rapid KVO updates and applies at 30fps
    // instead of immediately on every callback (~60fps). Halves body evaluations.
    private var kvoDisplayLink: DisplayLinkProxy?
    private var pendingKVOPadding: CGFloat?

    /// Spring kept for potential future use (e.g., composer-only animation).
    static let keyboardSpring = Animation.spring(duration: 0.25, bounce: 0)

    private init() {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let window = scene.keyWindow ?? scene.windows.first {
            let bottom = window.safeAreaInsets.bottom
            bottomInset = bottom < 50 ? bottom : 34
        } else {
            bottomInset = 34
        }

        // KeyboardSyncedContainer handles keyboard via keyboardLayoutGuide (iOS < 26).
        // SwiftUI handles keyboard natively (iOS 26+).
        // No notification/CADisplayLink/KVO tracking needed for either version.
        // Legacy code below disabled — kept for reference.
        if false {
            // iOS < 26: sync view + CADisplayLink — reads keyboard's REAL position
            // each frame from the same CA transaction. Pixel-perfect sync.
            NotificationCenter.default
                .publisher(for: UIResponder.keyboardWillChangeFrameNotification)
                .sink { [weak self] in self?.handleNotification($0) }
                .store(in: &cancellables)

            // Pre-create display link (paused) — avoids allocation overhead on first keyboard show.
            displayLinkProxy = DisplayLinkProxy { [weak self] in
                self?.animationTick()
            }
            displayLinkProxy?.isPaused = true
        }
    }

    /// Sets keyboardPadding with animation matching keyboard duration.
    /// No CADisplayLink, no sync views — just withAnimation to match timing.
    private func handleHeightChange(_ notification: Notification) {
        guard let info = notification.userInfo,
              let endFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
        else { return }

        let screenHeight = UIScreen.main.bounds.height
        let keyboardTop = endFrame.origin.y
        let isVisible = keyboardTop < screenHeight
        let targetPadding = isVisible ? max(0, screenHeight - keyboardTop - bottomInset) : 0
        let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25

        guard keyboardPadding != targetPadding else { return }

        // Animate spacer + composer offset to match keyboard animation.
        // Notification arrives ~10-30ms AFTER keyboard animation starts,
        // so we use a slightly shorter duration with a front-loaded curve
        // to keep input ahead of (or level with) the keyboard.
        // timingCurve approximates iOS keyboard curve 7 with faster start.
        let adjustedDuration = max(duration * 0.85, 0.12)
        withAnimation(.timingCurve(0.1, 0.9, 0.2, 1.0, duration: adjustedDuration)) {
            keyboardPadding = targetPadding
        }
    }

    /// Called from KVO — pixel-perfect interactive dismiss.
    /// Buffers values and applies at 30fps via CADisplayLink coalescing
    /// to reduce ChatDetailView.body evaluations during swipe-to-dismiss.
    func updateFromKVO(keyboardHeight: CGFloat) {
        PerformanceLogger.shared.track("keyboard.kvo")
        guard !isAnimating else { return }

        if keyboardHeight <= 0 {
            // Flush any pending KVO value and stop coalescing
            flushPendingKVO()
            stopKVOCoalescing()
            // Move composer to bottom immediately
            composerHostView?.setKeyboardOffset(0)

            if keyboardPadding != 0 {
                if pendingResetTask == nil {
                    pendingResetTask = Task { @MainActor [weak self] in
                        try? await Task.sleep(for: .milliseconds(150))
                        guard let self, !Task.isCancelled else { return }
                        if self.keyboardPadding != 0 {
                            self.keyboardPadding = 0
                        }
                        if self.spacerPadding != 0 {
                            self.spacerPadding = 0
                        }
                    }
                }
            }
            return
        }

        pendingResetTask?.cancel()
        pendingResetTask = nil

        // Round to nearest 2pt — sub-point changes are invisible but still
        // trigger full ChatDetailView.body evaluations.
        let rawPadding = max(0, keyboardHeight - bottomInset)
        let newPadding = round(rawPadding / 2) * 2

        // Only track decreasing padding (swipe-to-dismiss)
        let current = pendingKVOPadding ?? keyboardPadding
        guard newPadding < current else { return }

        // Move composer immediately (UIKit, no SwiftUI overhead)
        composerHostView?.setKeyboardOffset(rawPadding)

        // Buffer spacer update (30fps coalescing)
        pendingKVOPadding = newPadding

        // Start coalescing display link if not running
        if kvoDisplayLink == nil {
            kvoDisplayLink = DisplayLinkProxy(maxFPS: 30) { [weak self] in
                self?.applyPendingKVO()
            }
        }
    }

    /// Called by kvoDisplayLink at 30fps — applies buffered KVO value.
    private func applyPendingKVO() {
        guard let pending = pendingKVOPadding else {
            // No pending value — stop the display link
            stopKVOCoalescing()
            return
        }
        pendingKVOPadding = nil
        // Guard: KVO can produce NaN during edge cases (view hierarchy changes)
        guard pending.isFinite, pending >= 0 else { return }
        guard pending != keyboardPadding else { return }
        keyboardPadding = pending
        // Also update spacerPadding so KeyboardSpacer follows during interactive dismiss
        if pending != spacerPadding {
            spacerPadding = pending
        }
    }

    /// Immediately applies any buffered KVO value (used when KVO stops).
    private func flushPendingKVO() {
        guard let pending = pendingKVOPadding else { return }
        pendingKVOPadding = nil
        if pending != keyboardPadding {
            keyboardPadding = pending
        }
    }

    private func stopKVOCoalescing() {
        kvoDisplayLink?.stop()
        kvoDisplayLink = nil
    }

    private func handleNotification(_ notification: Notification) {
        guard let info = notification.userInfo,
              let endFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
        else { return }

        isAnimating = true
        // Stop KVO coalescing — notification animation takes over
        flushPendingKVO()
        stopKVOCoalescing()

        let screenHeight = UIScreen.main.bounds.height
        let keyboardTop = endFrame.origin.y
        let isVisible = keyboardTop < screenHeight
        let endHeight = isVisible ? (screenHeight - keyboardTop) : 0

        pendingResetTask?.cancel()
        pendingResetTask = nil

        // +8pt gap above keyboard — visible breathing room between composer and
        // keyboard edge. 4pt was invisible (1.3mm), 8pt = 2.6mm, clearly visible.
        // Baked into target so it's part of the smooth animation (no discontinuity).
        let targetPadding = isVisible ? max(0, endHeight - bottomInset + 8) : 0
        let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0.25
        let curveRaw = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int ?? 0

        // Configure bezier early — needed for HIDE head-start and animationTick fallback.
        configureBezier(curveRaw: curveRaw)

        // Direction-dependent initialization:
        let isShow = targetPadding > keyboardPadding
        if isShow {
            // Head-start: immediately set spacerPadding to ~1 frame into animation.
            // Covers the gap before first CADisplayLink tick fires (~8ms on 120Hz).
            // Without this, spacerPadding = 0 for the first frame → messages overlap composer.
            let initialT = min(0.016 / max(duration, 0.05), 1.0)
            let initialEased = cubicBezierEase(initialT)
            let initialValue = keyboardPadding + (targetPadding - keyboardPadding) * initialEased
            spacerPadding = max(keyboardPadding, round(initialValue))
        } else {
            // spacerPadding handled by animationTick via curve prediction
            // Keep keyboardPadding headstart for iOS 26+ consumers
            let initialT = min(0.016 / max(duration, 0.05), 1.0)
            let initialEased = cubicBezierEase(initialT)
            let initialValue = keyboardPadding + (targetPadding - keyboardPadding) * initialEased
            keyboardPadding = round(initialValue)
        }

        let delta = targetPadding - lastNotificationPadding
        lastNotificationPadding = targetPadding

        PerformanceLogger.shared.track("keyboard.notification")
        // Filter spurious notifications (delta=0, duration=0) — keyboard frame
        // didn't actually change. Happens on some iOS versions during text input.
        guard abs(delta) > 1 || targetPadding != keyboardPadding else {
            return
        }

        // UIKit compositor: animate in the SAME CA transaction as the keyboard.
        // This ensures pixel-perfect sync — render server interpolates both
        // the keyboard and compositor with identical timing curves.
        // SwiftUI ComposerOverlay (iOS 26+) doesn't use this — composerHostView is nil.
        if let hostView = composerHostView {
            let options = UIView.AnimationOptions(rawValue: UInt(curveRaw) << 16)
            UIView.animate(withDuration: duration, delay: 0, options: [options]) {
                hostView.setKeyboardOffset(targetPadding)
            }
        }

        // CADisplayLink animation for keyboardPadding (drives KeyboardSpacer).
        // Spacer lag is invisible because messages are scroll-anchored to bottom.
        if abs(delta) > 1, targetPadding != keyboardPadding {
            isAnimatingKeyboard = true
            startPaddingAnimation(to: targetPadding, duration: duration, curveRaw: curveRaw)
        } else if keyboardPadding != targetPadding {
            keyboardPadding = targetPadding
        }

        // Unblock KVO after keyboard settles.
        let unblockDelay = max(duration, 0.05) + 0.15
        Task { @MainActor [weak self] in
            try? await Task.sleep(for: .seconds(unblockDelay))
            self?.isAnimating = false
        }
    }

    // MARK: - Stepped Animation (4 keyframes)

    private var steppedAnimationTask: Task<Void, Never>?

    /// Animates keyboardPadding in 4 steps over ~duration.
    /// Each step triggers 1 layout pass. Total = 4 passes instead of 15.
    /// Visually smooth enough for 250ms keyboard animation.
    private func startSteppedAnimation(to target: CGFloat, duration: CFTimeInterval) {
        steppedAnimationTask?.cancel()
        let start = keyboardPadding
        let safeDuration = max(duration, 0.1)
        let steps = 4
        let stepDelay = safeDuration / Double(steps)

        steppedAnimationTask = Task { @MainActor [weak self] in
            for i in 1...steps {
                guard !Task.isCancelled, let self else { return }
                let fraction = Double(i) / Double(steps)
                // Ease-out curve: fast start, slow end
                let eased = 1 - pow(1 - fraction, 2)
                let value = max(0, round(start + (target - start) * eased))
                if value != self.keyboardPadding {
                    PerformanceLogger.shared.track("keyboard.step")
                    self.keyboardPadding = value
                }
                if i < steps {
                    try? await Task.sleep(for: .milliseconds(Int(stepDelay * 1000)))
                }
            }
            // Ensure we hit the exact target
            guard let self, !Task.isCancelled else { return }
            if self.keyboardPadding != target {
                self.keyboardPadding = max(0, target)
            }
        }
    }

    // MARK: - CADisplayLink animation (sync view + bezier fallback)

    // Sync view: hidden UIView animated with keyboard's exact curve.
    // Reading its presentation layer gives the real easing value — no guessing.
    private var syncView: UIView?

    private func startPaddingAnimation(to target: CGFloat, duration: CFTimeInterval, curveRaw: Int) {
        animationNumber += 1
        animStartPadding = keyboardPadding
        animTargetPadding = target
        animStartTime = CACurrentMediaTime()
        animDuration = max(duration, 0.05)
        animTickCount = 0
        lastEased = 0
        previousMonotonicEased = 0

        // Always configure bezier — needed for spacerPadding advance computation
        // even when sync view is the primary easing source.
        configureBezier(curveRaw: curveRaw)

        // Primary: sync view matches keyboard's exact curve (same CA transaction).
        let _ = setupSyncAnimation(duration: duration, curveRaw: curveRaw)

        // Reuse existing display link to preserve vsync phase alignment.
        if let proxy = displayLinkProxy {
            proxy.isPaused = false
        } else {
            displayLinkProxy = DisplayLinkProxy { [weak self] in
                self?.animationTick()
            }
        }
    }

    /// Creates a hidden UIView animated with the keyboard's exact curve.
    private func setupSyncAnimation(duration: CFTimeInterval, curveRaw: Int) -> Bool {
        guard let window = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene }).first?.keyWindow else {
            return false
        }

        syncView?.layer.removeAllAnimations()
        syncView?.removeFromSuperview()

        let view = UIView(frame: CGRect(x: -10, y: -10, width: 1, height: 1))
        view.alpha = 0
        window.addSubview(view)
        syncView = view

        let options = UIView.AnimationOptions(rawValue: UInt(curveRaw) << 16)
        UIView.animate(withDuration: duration, delay: 0, options: [options]) {
            view.alpha = 1
        }

        return true
    }

    private func animationTick() {
        PerformanceLogger.shared.track("keyboard.animTick")
        animTickCount += 1

        let now = CACurrentMediaTime()
        lastTickTime = now

        let elapsed = now - animStartTime

        // Read eased fraction: sync view (exact) or bezier (fallback).
        var eased: CGFloat
        if let presentation = syncView?.layer.presentation() {
            eased = min(max(CGFloat(presentation.opacity), 0), 1)
        } else {
            let t = min(elapsed / animDuration, 1.0)
            eased = cubicBezierEase(t)
        }

        // Enforce monotonic progress — sync view's presentation layer can give
        // non-monotonic values in the first 2 ticks while Core Animation commits
        // the animation to the render server. Without this guard, padding oscillates
        // (e.g. 312 → 306 → 310 → 302) causing a visible "jerk".
        if eased < lastEased {
            eased = lastEased
        }
        lastEased = eased

        guard eased.isFinite else {
            displayLinkProxy?.isPaused = true
            lastTickTime = 0
            isAnimatingKeyboard = false
            return
        }

        let rawEased = eased
        let prevMonotonicEased = previousMonotonicEased

        // Velocity prediction: compensates for SwiftUI's ~8ms render lag.
        // Only for HIDE — SHOW uses spacerPadding advance instead (no compound overshoot).
        if animTickCount > 1 && animTargetPadding < animStartPadding {
            let velocity = eased - prevMonotonicEased
            if velocity > 0 {
                eased = min(eased + velocity * 1.0, 1.0)
            }
        }
        previousMonotonicEased = rawEased

        let raw = animStartPadding + (animTargetPadding - animStartPadding) * eased

        // Smooth glide: once past animDuration, blend sync-view-driven value
        // with exponential approach to target. Each tick closes 40% of remaining
        // gap → converges smoothly instead of snapping 6-16pt at the deadline.
        let remaining = abs(animTargetPadding - raw)
        let pastDuration = elapsed > animDuration
        let effectiveRaw: CGFloat
        if pastDuration && remaining > 1 {
            // Blend: 40% toward target each tick (exponential decay, ~3 ticks to <1pt)
            let current = keyboardPadding
            let step = (animTargetPadding - current) * 0.4
            effectiveRaw = current + step
        } else {
            effectiveRaw = raw
        }
        let rounded = max(0, round(effectiveRaw))

        // Hard deadline: abort after 500ms or 40 ticks (safety net).
        let hardDeadline = elapsed >= animDuration + 0.25
        let closeEnough = abs(animTargetPadding - rounded) <= 1
        if hardDeadline || animTickCount > 40 || (pastDuration && closeEnough) {
            keyboardPadding = max(0, animTargetPadding)
            spacerPadding = max(0, animTargetPadding)
            // Pause instead of invalidate — preserves vsync phase for next animation.
            displayLinkProxy?.isPaused = true
            lastTickTime = 0
            isAnimatingKeyboard = false
        } else if rounded != keyboardPadding {
            keyboardPadding = rounded
        }

        // Curve-predicted spacerPadding: predicts where the animation will be
        // when SwiftUI finishes rendering (~8-16ms from now).
        // SHOW: 2.5-frame advance — spacer must stay AHEAD of composer (UIKit)
        //   to prevent message overlap. SwiftUI pipeline lag = ~8ms + render.
        // HIDE: 0 advance — SwiftUI lag keeps spacer slightly ABOVE compositor,
        //   creating a small gap instead of overlap. Gap > overlap.
        let frameInterval = displayLinkProxy?.currentFrameInterval ?? 0.008
        let isShowDirection = animTargetPadding > animStartPadding
        let advanceFrames: CGFloat = isShowDirection ? 2.5 : 0
        let predictedElapsed = elapsed + frameInterval * advanceFrames
        let predictedT = min(predictedElapsed / animDuration, 1.0)
        let predictedEased = cubicBezierEase(predictedT)
        let predictedSpacer = animStartPadding + (animTargetPadding - animStartPadding) * predictedEased
        var predictedRounded = max(0, round(predictedSpacer))
        // During SHOW: floor at current keyboardPadding — absolute guarantee of no overlap.
        if isShowDirection {
            predictedRounded = max(predictedRounded, rounded)
        }
        if predictedRounded != spacerPadding {
            spacerPadding = predictedRounded
        }

    }

    // MARK: - Cubic bezier fallback

    /// Maps `UIViewAnimationCurve` rawValue to cubic bezier control points.
    private func configureBezier(curveRaw: Int) {
        switch UIView.AnimationCurve(rawValue: curveRaw) {
        case .easeIn:
            bezierP1x = 0.42; bezierP1y = 0
            bezierP2x = 1.0;  bezierP2y = 1.0
        case .easeOut:
            bezierP1x = 0;    bezierP1y = 0
            bezierP2x = 0.58; bezierP2y = 1.0
        case .linear:
            bezierP1x = 0;    bezierP1y = 0
            bezierP2x = 1.0;  bezierP2y = 1.0
        default:
            // CSS "ease" — approximation of iOS keyboard curve 7.
            // Only used as fallback when sync view is unavailable.
            bezierP1x = 0.25; bezierP1y = 0.1
            bezierP2x = 0.25; bezierP2y = 1.0
        }
    }

    /// Evaluates the configured cubic bezier at linear time `x` (0…1).
    /// Uses Newton–Raphson for fast convergence (~4 iterations).
    private func cubicBezierEase(_ x: CGFloat) -> CGFloat {
        guard x > 0 else { return 0 }
        guard x < 1 else { return 1 }
        var t = x
        for _ in 0..<8 {
            let bx = bezierValue(t, p1: bezierP1x, p2: bezierP2x)
            let dx = bezierDerivative(t, p1: bezierP1x, p2: bezierP2x)
            guard abs(dx) > 1e-6 else { break }
            t -= (bx - x) / dx
            t = min(max(t, 0), 1)
        }
        return bezierValue(t, p1: bezierP1y, p2: bezierP2y)
    }

    /// Single axis of cubic bezier: B(t) = 3(1−t)²t·p1 + 3(1−t)t²·p2 + t³
    private func bezierValue(_ t: CGFloat, p1: CGFloat, p2: CGFloat) -> CGFloat {
        let mt = 1 - t
        return 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t
    }

    /// Derivative of single bezier axis: B'(t)
    private func bezierDerivative(_ t: CGFloat, p1: CGFloat, p2: CGFloat) -> CGFloat {
        let mt = 1 - t
        return 3 * mt * mt * p1 + 6 * mt * t * (p2 - p1) + 3 * t * t * (1 - p2)
    }
}

// MARK: - CADisplayLink wrapper (avoids @objc requirement on @MainActor class)

private class DisplayLinkProxy {
    private var callback: (() -> Void)?
    private var displayLink: CADisplayLink?

    /// Frame interval from CADisplayLink (targetTimestamp - timestamp).
    /// Used for curve prediction in animationTick.
    private(set) var currentFrameInterval: CFTimeInterval = 0.008

    /// - Parameter maxFPS: Max frame rate. 0 = device native (120Hz on ProMotion).
    ///   Non-zero values cap via preferredFrameRateRange.
    init(maxFPS: Int = 0, callback: @escaping () -> Void) {
        self.callback = callback
        self.displayLink = CADisplayLink(target: self, selector: #selector(tick))
        if maxFPS > 0 {
            let fps = Float(maxFPS)
            self.displayLink?.preferredFrameRateRange = CAFrameRateRange(
                minimum: fps / 2, maximum: fps, preferred: fps
            )
        }
        // maxFPS == 0: no range set → runs at device native refresh rate (120Hz ProMotion)
        self.displayLink?.add(to: .main, forMode: .common)
    }

    @objc private func tick() {
        if let dl = displayLink {
            currentFrameInterval = dl.targetTimestamp - dl.timestamp
        }
        callback?()
    }

    var isPaused: Bool {
        get { displayLink?.isPaused ?? true }
        set { displayLink?.isPaused = newValue }
    }

    func stop() {
        displayLink?.invalidate()
        displayLink = nil
        callback = nil
    }

    deinit {
        stop()
    }
}
