//
//  TVPlayerProgressBar.swift
//  Yattee
//
//  Focusable progress bar for tvOS with smooth touchpad scrubbing support.
//

#if os(tvOS)
import SwiftUI
import UIKit

/// Progress bar with smooth scrubbing support for tvOS Siri Remote touchpad.
struct TVPlayerProgressBar: View {
    let currentTime: TimeInterval
    let duration: TimeInterval
    let bufferedTime: TimeInterval
    let storyboard: Storyboard?
    let chapters: [VideoChapter]
    let onSeek: (TimeInterval) -> Void
    /// Called when scrubbing state changes - parent should stop auto-hide timer when true
    var onScrubbingChanged: ((Bool) -> Void)?
    /// Whether the current stream is live
    let isLive: Bool
    /// Whether to show chapter markers on the progress bar (default: true)
    var showChapters: Bool = true
    /// SponsorBlock segments to display on the progress bar.
    var sponsorSegments: [SponsorBlockSegment] = []
    /// Settings for SponsorBlock segment display.
    var sponsorBlockSettings: SponsorBlockSegmentSettings = .default
    /// Color for the played portion of the progress bar.
    var playedColor: Color = .red
    /// Pending target time from the parent's accumulating remote-seek flow
    /// (arrow presses while the bar is focused but not in SELECT scrub mode).
    /// When set, the handle and played portion reflect this value.
    var remoteSeekTime: TimeInterval? = nil
    /// Called when the bar is focused (not scrubbing) and user presses left/right.
    /// Parameter: `forward` — true for right, false for left.
    var onRemoteSeek: ((Bool) -> Void)? = nil
    /// Parent bumps this to request the bar to cancel any in-progress scrub
    /// without performing a seek (used for the Menu button).
    var cancelScrubTrigger: UUID? = nil

    /// Track focus state internally.
    @FocusState private var isFocused: Bool

    /// Time during active scrubbing (nil when not scrubbing).
    @State private var scrubTime: TimeInterval?

    /// Whether user is actively scrubbing.
    @State private var isScrubbing = false

    /// Accumulated pan translation for scrubbing.
    @State private var panAccumulator: CGFloat = 0

    /// Consecutive-event streak for rapid D-pad/touchpad scrubbing.
    /// tvOS routes touchpad swipes as rapid `onMoveCommand` events, so we
    /// amplify step size when events arrive in quick succession.
    @State private var dpadStreakCount: Int = 0
    @State private var lastDPadTime: Date?
    @State private var lastDPadDirection: MoveCommandDirection?

    /// Whether the arrow-seek storyboard/chapter preview is currently shown.
    @State private var showArrowSeekPreview = false
    /// Delayed-hide task for the arrow-seek preview after the user stops pressing arrows.
    @State private var arrowSeekPreviewHideTask: Task<Void, Never>?

    /// The time to display. SELECT-based scrub takes priority, then the
    /// parent's pending remote-seek target, then the actual playback time.
    private var displayTime: TimeInterval {
        scrubTime ?? remoteSeekTime ?? currentTime
    }

    /// Progress as a fraction (0-1).
    private var progress: Double {
        guard duration > 0 else { return 0 }
        return min(max(displayTime / duration, 0), 1)
    }

    /// Buffered progress as a fraction (0-1).
    private var bufferedProgress: Double {
        guard duration > 0 else { return 0 }
        return min(bufferedTime / duration, 1)
    }

    var body: some View {
        Button {
            if isScrubbing {
                commitScrub()
            } else if !isLive {
                enterScrubMode()
            }
        } label: {
            progressContent
        }
        .buttonStyle(TVProgressBarButtonStyle(isFocused: isFocused))
        .disabled(isLive)
        .overlay {
            // Gesture capture layer (only when scrubbing). Siri Remote pan
            // gestures are indirect touches, so matching the button's size
            // is sufficient — no need to expand and disturb parent layout.
            if isScrubbing {
                TVPanGestureView(
                    onPanChanged: { translation, velocity in
                        handlePan(translation: translation, velocity: velocity)
                    },
                    onPanEnded: {
                        handlePanEnded()
                    }
                )
            }
        }
        .focused($isFocused)
        .onMoveCommand { direction in
            if isScrubbing {
                // D-pad fallback while in SELECT-based scrub mode.
                handleDPad(direction: direction)
            } else if !isLive, direction == .left || direction == .right {
                // Focused but not scrubbing: delegate accumulating remote seek
                // to parent. Up/down falls through to normal focus navigation.
                onRemoteSeek?(direction == .right)
            }
        }
        .onChange(of: isFocused) { _, focused in
            if !focused {
                commitScrub()
            }
        }
        .onChange(of: cancelScrubTrigger) { _, newValue in
            guard newValue != nil, isScrubbing else { return }
            cancelScrub()
        }
        .onChange(of: remoteSeekTime) { _, newValue in
            if newValue != nil {
                arrowSeekPreviewHideTask?.cancel()
                arrowSeekPreviewHideTask = nil
                if !showArrowSeekPreview {
                    withAnimation(.easeOut(duration: 0.15)) {
                        showArrowSeekPreview = true
                    }
                }
            } else {
                scheduleArrowSeekPreviewHide()
            }
        }
        .onChange(of: isScrubbing) { _, scrubbing in
            if scrubbing {
                arrowSeekPreviewHideTask?.cancel()
                arrowSeekPreviewHideTask = nil
                if showArrowSeekPreview {
                    withAnimation(.easeOut(duration: 0.15)) {
                        showArrowSeekPreview = false
                    }
                }
            }
        }
        .animation(.easeInOut(duration: 0.2), value: isFocused)
        .animation(.easeInOut(duration: 0.1), value: isScrubbing)
    }

    private func enterScrubMode() {
        scrubTime = currentTime
        panAccumulator = 0
        lastLiveSeekTime = nil
        withAnimation(.easeOut(duration: 0.15)) {
            isScrubbing = true
        }
        onScrubbingChanged?(true)
        scheduleIdleAutoCommit()
    }

    private var progressContent: some View {
        VStack(spacing: 12) {
            // Progress bar (hide for live streams)
            if !isLive {
                GeometryReader { geometry in
                    ZStack(alignment: .leading) {
                        // Progress bar with chapter segments (4pt gaps for tvOS visibility)
                        SegmentedProgressBar(
                            chapters: showChapters ? chapters : [],
                            duration: duration,
                            currentTime: displayTime,
                            bufferedTime: bufferedTime,
                            height: isFocused ? (isScrubbing ? 16 : 12) : 6,
                            gapWidth: 4,
                            playedColor: isFocused ? playedColor : .white,
                            bufferedColor: .white.opacity(0.4),
                            backgroundColor: .white.opacity(0.2),
                            sponsorSegments: sponsorSegments,
                            sponsorBlockSettings: sponsorBlockSettings
                        )

                        // Scrub handle (visible when focused)
                        if isFocused {
                            Circle()
                                .fill(.white)
                                .frame(width: isScrubbing ? 32 : 24, height: isScrubbing ? 32 : 24)
                                .shadow(color: .black.opacity(0.3), radius: 4)
                                .offset(x: (geometry.size.width * progress) - (isScrubbing ? 16 : 12))
                                .animation(.easeOut(duration: 0.1), value: progress)
                        }
                    }
                    .frame(maxHeight: .infinity, alignment: .center)
                    .overlay(alignment: .top) {
                        scrubPreviewOverlay(geometry: geometry)
                    }
                }
                .frame(height: 20)
            }

            // Time labels
            HStack {
                // Current time or LIVE indicator
                if isLive {
                    HStack(spacing: 6) {
                        Circle()
                            .fill(.red)
                            .frame(width: 8, height: 8)
                        Text(String(localized: "player.live"))
                            .font(.subheadline.weight(.semibold))
                            .foregroundStyle(.red)
                    }
                } else {
                    Text(displayTime.formattedAsTimestamp)
                        .monospacedDigit()
                        .font(.subheadline)
                        .fontWeight(isScrubbing ? .semibold : .regular)
                        .foregroundStyle(.white)
                }

                Spacer()

                // Remaining time (only for non-live)
                if !isLive {
                    Text("-\(max(0, duration - displayTime).formattedAsTimestamp)")
                        .monospacedDigit()
                        .font(.subheadline)
                        .foregroundStyle(.white.opacity(0.7))
                }
            }

        }
    }

    @ViewBuilder
    private func scrubPreviewOverlay(geometry: GeometryProxy) -> some View {
        // Storyboard panel: only for arrow-seek (accumulator mode). During
        // SELECT-based scrub we live-seek the underlying frame, so the
        // storyboard would just duplicate what's already on screen.
        // Chapter capsule: shown for either scrub mode — it conveys info the
        // raw frame doesn't (chapter title) and stays useful during live seek.
        let seekTime = displayTime
        let currentChapter = showChapters ? chapters.last(where: { $0.startTime <= seekTime }) : nil
        // Storyboard panel is 320 thumbnail + 4pt horizontal padding * 2 = 328, plus shadow.
        // Use a slightly larger clamp width so the shadow stays on screen.
        let panelWidth: CGFloat = 344
        // Panel height: thumbnail 180 + 4pt vertical padding * 2 = 188 (round up for shadow).
        let panelHeight: CGFloat = 200
        let capsuleSpacing: CGFloat = 8
        // Approximate capsule height (24pt text + 6pt padding * 2 + shadow) — used only
        // for vertical positioning, not for layout sizing.
        let capsuleApproxHeight: CGFloat = 44

        let xTarget = geometry.size.width * progress
        let halfPanel = panelWidth / 2
        let clampedPanelX = max(halfPanel, min(geometry.size.width - halfPanel, xTarget))
        let panelCenterY = -panelHeight / 2 - 16
        // When the storyboard panel is hidden, place the capsule where the
        // panel would have been so it doesn't float far above the bar.
        let capsuleCenterY = showArrowSeekPreview
            ? -panelHeight - 16 - capsuleSpacing - capsuleApproxHeight / 2
            : -16 - capsuleApproxHeight / 2

        ZStack {
            if showArrowSeekPreview {
                Group {
                    if let storyboard {
                        TVSeekPreviewView(
                            storyboard: storyboard,
                            seekTime: seekTime
                        )
                    } else {
                        Text(seekTime.formattedAsTimestamp)
                            .font(.system(size: 48, weight: .medium))
                            .monospacedDigit()
                            .foregroundStyle(.white)
                            .padding(.horizontal, 32)
                            .padding(.vertical, 16)
                            .background(
                                RoundedRectangle(cornerRadius: 16)
                                    .fill(.ultraThinMaterial)
                            )
                    }
                }
                .fixedSize()
                .position(x: clampedPanelX, y: panelCenterY)
                .transition(.scale.combined(with: .opacity))
            }

            if (isScrubbing || showArrowSeekPreview), let currentChapter {
                TVChapterCapsuleView(title: currentChapter.title)
                    .positioned(xTarget: xTarget, availableWidth: geometry.size.width)
                    .position(x: geometry.size.width / 2, y: capsuleCenterY)
                    .transition(.opacity)
            }
        }
        .allowsHitTesting(false)
    }

    // MARK: - Pan Gesture Handling

    private func handlePan(translation: CGFloat, velocity: CGFloat) {
        guard duration > 0, isScrubbing else { return }

        scheduleIdleAutoCommit()

        // Calculate scrub sensitivity based on duration
        // Lower values = slower/finer scrubbing
        let baseSensitivity: CGFloat
        if duration > 3600 {
            baseSensitivity = duration / 1500
        } else if duration > 600 {
            baseSensitivity = duration / 2000
        } else {
            baseSensitivity = duration / 3000
        }

        // Non-linear velocity response: slow swipes stay precise, fast flicks accelerate.
        let normalizedVelocity = abs(velocity) / 500
        let velocityMultiplier = min(max(pow(normalizedVelocity, 1.4), 0.3), 6.0)
        let adjustedSensitivity = baseSensitivity * velocityMultiplier

        // Update scrub time based on translation delta
        let delta = translation - panAccumulator
        panAccumulator = translation

        let timeChange = TimeInterval(delta * adjustedSensitivity)
        let currentScrubTime = scrubTime ?? currentTime
        scrubTime = min(max(0, currentScrubTime + timeChange), duration)

        scheduleLiveSeek()
    }

    private func handlePanEnded() {
        // Reset accumulator for next swipe
        panAccumulator = 0
        // Flush any pending throttled seek so the underlying frame catches up
        // immediately when the user lifts their finger.
        flushLiveSeek()
        scheduleIdleAutoCommit()
    }

    @State private var seekTask: Task<Void, Never>?
    @State private var lastLiveSeekTime: Date?

    /// Live-seek throttle: ~150ms leading + trailing.
    /// Keeps the underlying frame in sync with the storyboard preview while
    /// scrubbing without flooding the backend with seek calls.
    private func scheduleLiveSeek() {
        let interval: TimeInterval = 0.15
        let now = Date()
        let elapsed = lastLiveSeekTime.map { now.timeIntervalSince($0) } ?? .infinity

        if elapsed >= interval {
            seekTask?.cancel()
            seekTask = nil
            lastLiveSeekTime = now
            if let time = scrubTime {
                onSeek(time)
            }
        } else if seekTask == nil {
            let delay = interval - elapsed
            seekTask = Task { @MainActor in
                try? await Task.sleep(for: .seconds(delay))
                guard !Task.isCancelled else { return }
                seekTask = nil
                lastLiveSeekTime = Date()
                if let time = scrubTime {
                    onSeek(time)
                }
            }
        }
    }

    private func flushLiveSeek() {
        seekTask?.cancel()
        seekTask = nil
        lastLiveSeekTime = Date()
        if let time = scrubTime {
            onSeek(time)
        }
    }

    // MARK: - Idle auto-commit

    /// Auto-commit timer: AVPlayerViewController commits scrub mode after a
    /// few seconds of inactivity so playback can resume.
    @State private var idleAutoCommitTask: Task<Void, Never>?

    private func scheduleIdleAutoCommit() {
        idleAutoCommitTask?.cancel()
        idleAutoCommitTask = Task { @MainActor in
            try? await Task.sleep(for: .seconds(3))
            guard !Task.isCancelled, isScrubbing else { return }
            commitScrub()
        }
    }

    private func cancelIdleAutoCommit() {
        idleAutoCommitTask?.cancel()
        idleAutoCommitTask = nil
    }

    // MARK: - D-Pad Fallback

    private func handleDPad(direction: MoveCommandDirection) {
        guard duration > 0, isScrubbing else { return }

        switch direction {
        case .left, .right:
            // Track event rate. When events arrive quickly in the same
            // direction (i.e. a touchpad swipe being rasterized into move
            // commands), grow the streak and scale the step size up.
            let now = Date()
            let gap = lastDPadTime.map { now.timeIntervalSince($0) } ?? .infinity

            // tvOS throttles onMoveCommand at ~300-400ms even during a fast
            // swipe, so we need a generous window to still recognize a burst.
            if gap < 0.5, lastDPadDirection == direction {
                dpadStreakCount = min(dpadStreakCount + 1, 30)
            } else {
                dpadStreakCount = 1
            }
            lastDPadTime = now
            lastDPadDirection = direction

            // Base step based on video length.
            let baseStep: TimeInterval
            if duration > 3600 {
                baseStep = 15
            } else if duration > 600 {
                baseStep = 8
            } else {
                baseStep = 5
            }

            // Steeper curve so a swipe (few events) actually covers ground.
            let streakMultiplier = pow(Double(dpadStreakCount), 1.6)
            let scrubAmount = baseStep * streakMultiplier

            let currentScrubTime = scrubTime ?? currentTime
            if direction == .left {
                scrubTime = max(0, currentScrubTime - scrubAmount)
            } else {
                scrubTime = min(duration, currentScrubTime + scrubAmount)
            }
            scheduleLiveSeek()
            scheduleIdleAutoCommit()

        case .up, .down:
            // Exit scrub mode and let navigation happen
            commitScrub()
            dpadStreakCount = 0
            lastDPadTime = nil
            lastDPadDirection = nil

        @unknown default:
            break
        }
    }

    // MARK: - Commit

    private func commitScrub() {
        seekTask?.cancel()
        seekTask = nil
        cancelIdleAutoCommit()

        let wasScrubbing = isScrubbing

        if let time = scrubTime {
            onSeek(time)
        }

        withAnimation(.easeOut(duration: 0.15)) {
            scrubTime = nil
            isScrubbing = false
            showArrowSeekPreview = false
        }
        panAccumulator = 0
        dpadStreakCount = 0
        lastDPadTime = nil
        lastDPadDirection = nil
        lastLiveSeekTime = nil
        arrowSeekPreviewHideTask?.cancel()
        arrowSeekPreviewHideTask = nil

        if wasScrubbing {
            onScrubbingChanged?(false)
        }
    }

    private func cancelScrub() {
        seekTask?.cancel()
        seekTask = nil
        cancelIdleAutoCommit()

        let wasScrubbing = isScrubbing

        withAnimation(.easeOut(duration: 0.15)) {
            scrubTime = nil
            isScrubbing = false
            showArrowSeekPreview = false
        }
        panAccumulator = 0
        dpadStreakCount = 0
        lastDPadTime = nil
        lastDPadDirection = nil
        lastLiveSeekTime = nil
        arrowSeekPreviewHideTask?.cancel()
        arrowSeekPreviewHideTask = nil

        if wasScrubbing {
            onScrubbingChanged?(false)
        }
    }

    private func scheduleArrowSeekPreviewHide() {
        arrowSeekPreviewHideTask?.cancel()
        arrowSeekPreviewHideTask = Task { @MainActor in
            try? await Task.sleep(for: .milliseconds(2000))
            guard !Task.isCancelled else { return }
            withAnimation(.easeOut(duration: 0.2)) {
                showArrowSeekPreview = false
            }
        }
    }

}

// MARK: - Pan Gesture View

/// UIKit view that captures pan gestures on the Siri Remote touchpad.
struct TVPanGestureView: UIViewRepresentable {
    let onPanChanged: (CGFloat, CGFloat) -> Void  // (translation, velocity)
    let onPanEnded: () -> Void

    func makeUIView(context: Context) -> TVPanGestureUIView {
        let view = TVPanGestureUIView()
        view.onPanChanged = onPanChanged
        view.onPanEnded = onPanEnded
        return view
    }

    func updateUIView(_ uiView: TVPanGestureUIView, context: Context) {
        uiView.onPanChanged = onPanChanged
        uiView.onPanEnded = onPanEnded
    }
}

class TVPanGestureUIView: UIView {
    var onPanChanged: ((CGFloat, CGFloat) -> Void)?
    var onPanEnded: (() -> Void)?

    private var panRecognizer: UIPanGestureRecognizer!

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupGesture()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupGesture()
    }

    private func setupGesture() {
        panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        panRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)]
        addGestureRecognizer(panRecognizer)

        // Make view focusable
        isUserInteractionEnabled = true
    }

    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: self).x
        let velocity = gesture.velocity(in: self).x

        switch gesture.state {
        case .began, .changed:
            onPanChanged?(translation, velocity)
        case .ended, .cancelled:
            onPanEnded?()
            gesture.setTranslation(.zero, in: self)
        default:
            break
        }
    }
}

// MARK: - Button Style

/// Button style for the progress bar.
struct TVProgressBarButtonStyle: ButtonStyle {
    let isFocused: Bool

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.98 : 1.0)
            .opacity(configuration.isPressed ? 0.9 : 1.0)
    }
}

#endif
