import Lottie
import UIKit

// MARK: - MessageVoiceView

/// Voice message bubble content: play/pause button + waveform bars + duration.
/// Telegram parity from ChatMessageInteractiveFileNode.swift audit.
final class MessageVoiceView: UIView {

    // MARK: - Subviews

    private let playButton = UIButton(type: .system)
    private let playPauseAnimationView = LottieAnimationView()
    private let downloadArrowView = UIImageView()  // ↓ arrow before download
    private let downloadRingView = VoiceDownloadRingView()
    private let waveformView = WaveformView()
    private let durationLabel = UILabel()

    // MARK: - State

    private var messageId: String = ""
    private var attachmentId: String = ""
    private var isOutgoing = false
    private var isShowingPause = false  // tracks Lottie visual state
    private var isDownloaded = false    // false = show ↓, true = show play/pause
    private var totalDuration: TimeInterval = 0  // original duration for label reset
    /// Center of play button in this view's coordinate space (for external blob positioning).
    var playButtonCenter: CGPoint { playButton.center }
    /// Whether the waveform is accepting scrub gestures (true during playback).
    var isScrubbingEnabled: Bool { waveformView.enableScrubbing }
    /// Frame of the waveform in this view's coordinate space.
    var waveformFrame: CGRect { waveformView.frame }

    // MARK: - Layout Constants (Telegram exact: ChatMessageInteractiveFileNode)

    private let playButtonSize: CGFloat = 44
    private let playButtonLeading: CGFloat = 3
    private let waveformX: CGFloat = 57   // Telegram: x=57
    private let waveformY: CGFloat = 1    // Telegram: y=1
    private let waveformHeight: CGFloat = 18  // Telegram: peakHeight=18 in component
    private let durationX: CGFloat = 56   // Telegram: x=56
    private let durationY: CGFloat = 22   // Telegram: y=22

    // MARK: - Init

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

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

    // Playback
    var onPlayTapped: (() -> Void)?
    /// Download cancel callback (forwarded from VoiceDownloadRingView).
    var onDownloadCancel: (() -> Void)?

    // MARK: - Setup

    private func setupSubviews() {
        playButton.layer.cornerRadius = playButtonSize / 2
        playButton.clipsToBounds = true
        playButton.addTarget(self, action: #selector(playTapped), for: .touchUpInside)
        addSubview(playButton)

        // Lottie play/pause animation (same asset as RecordingPreviewPanel)
        playPauseAnimationView.backgroundBehavior = .pauseAndRestore
        playPauseAnimationView.contentMode = .scaleAspectFit
        playPauseAnimationView.isUserInteractionEnabled = false
        playButton.addSubview(playPauseAnimationView)
        if let animation = LottieAnimation.named(VoiceRecordingLottieAsset.playPause.rawValue) {
            playPauseAnimationView.animation = animation
            playPauseAnimationView.currentFrame = 0  // start at play icon
        } else {
            // Fallback: SF Symbol if Lottie asset missing
            let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .bold)
            playButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
        }

        // Download arrow (shown before voice is downloaded)
        let arrowConfig = UIImage.SymbolConfiguration(pointSize: 16, weight: .bold)
        downloadArrowView.image = UIImage(systemName: "arrow.down", withConfiguration: arrowConfig)
        downloadArrowView.contentMode = .center
        downloadArrowView.isUserInteractionEnabled = false
        playButton.addSubview(downloadArrowView)

        // Download progress ring (overlays play button during CDN download)
        downloadRingView.onCancel = { [weak self] in self?.onDownloadCancel?() }
        addSubview(downloadRingView)

        waveformView.peakHeight = 18    // Telegram AudioWaveformComponent peak
        waveformView.distance = 2.0    // Telegram AudioWaveformComponent (bubble context)
        waveformView.gravity = .bottom  // Telegram: bars grow upward from bottom
        waveformView.onSeek = { [weak self] fraction in
            guard self != nil else { return }
            VoiceMessagePlayer.shared.seek(to: fraction)
        }
        addSubview(waveformView)

        durationLabel.font = .monospacedDigitSystemFont(ofSize: 11, weight: .regular)
        addSubview(durationLabel)
    }

    // MARK: - Layout (Telegram exact: play at x=3,y=1; waveform at x=57,y=1; duration at x=56,y=22)

    override func layoutSubviews() {
        super.layoutSubviews()
        let h = bounds.height

        // Play button: 44×44, Telegram x=3, y=centered in cell
        playButton.frame = CGRect(
            x: playButtonLeading,
            y: max(0, (h - playButtonSize) / 2),
            width: playButtonSize,
            height: playButtonSize
        )
        // Lottie inset inside 44×44 button (26×26 centered)
        let lottieInset: CGFloat = 9
        playPauseAnimationView.frame = CGRect(
            x: lottieInset, y: lottieInset,
            width: playButtonSize - lottieInset * 2,
            height: playButtonSize - lottieInset * 2
        )
        // Download arrow: same frame as button interior
        downloadArrowView.frame = playButton.bounds

        // Download ring: same frame as play button
        downloadRingView.frame = playButton.frame

        // Waveform: from x=57 to near right edge, height=18, y=1
        let waveW = bounds.width - waveformX - 4
        waveformView.frame = CGRect(
            x: waveformX,
            y: waveformY,
            width: max(0, waveW),
            height: waveformHeight
        )
        // Force redraw after layout — frame change alone may not trigger display
        // when the size stays the same between reuse cycles.
        waveformView.setNeedsDisplay()

        // Duration: at x=56, y=22
        durationLabel.frame = CGRect(
            x: durationX,
            y: durationY,
            width: bounds.width - durationX - 4,
            height: 14
        )
    }

    // MARK: - Configure

    func configure(messageId: String, attachmentId: String, preview: String,
                   duration: TimeInterval, isOutgoing: Bool) {
        self.messageId = messageId
        self.attachmentId = attachmentId
        self.isOutgoing = isOutgoing
        self.isShowingPause = false

        // Reset Lottie to play icon (frame 0)
        if playPauseAnimationView.animation != nil {
            playPauseAnimationView.stop()
            playPauseAnimationView.currentFrame = 0
            playButton.setImage(nil, for: .normal)
        }

        self.totalDuration = duration

        // Decode waveform from preview — fallback to flat bars if empty
        var samples = Self.decodeWaveform(from: preview)
        #if DEBUG
        if samples.isEmpty {
            print("[VOICE_WAVEFORM] empty waveform — msgId=\(messageId.prefix(8))… preview='\(preview.prefix(80))'")
        }
        #endif
        if samples.isEmpty {
            // Telegram shows flat bars when waveform data is missing (better than empty space)
            samples = [Float](repeating: 0.3, count: 30)
        }
        waveformView.setSamples(samples)
        waveformView.progress = 0

        // Duration label
        durationLabel.text = Self.formatDuration(duration)

        // Telegram-exact theme-aware colors
        applyColors()
    }

    // MARK: - Colors (Telegram exact: DefaultDarkPresentationTheme + DefaultDayPresentationTheme)

    private func applyColors() {
        let isDark = traitCollection.userInterfaceStyle == .dark
        let colors = RosettaColors.Voice.colors(isOutgoing: isOutgoing, isDark: isDark)

        playButton.backgroundColor = colors.playButtonBg
        playButton.tintColor = colors.playButtonFg
        downloadArrowView.tintColor = colors.playButtonFg
        downloadRingView.setRingColor(colors.playButtonFg)
        durationLabel.textColor = colors.durationText
        waveformView.foregroundColor_ = colors.waveformPlayed
        waveformView.backgroundColor_ = colors.waveformUnplayed
        waveformView.setNeedsDisplay()

        // Tint Lottie animation icon (same pattern as RecordingPreviewPanel)
        applyPlayPauseTintColor(colors.playButtonFg)
    }

    private func applyPlayPauseTintColor(_ color: UIColor) {
        var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        color.getRed(&r, green: &g, blue: &b, alpha: &a)
        playPauseAnimationView.setValueProvider(
            ColorValueProvider(LottieColor(r: Double(r), g: Double(g), b: Double(b), a: Double(a))),
            keypath: AnimationKeypath(keypath: "**.Color")
        )
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            applyColors()
        }
    }

    // MARK: - Download State

    /// Set whether voice data is already downloaded (cached).
    /// `false` → show ↓ arrow. `true` → show play/pause icon.
    func setDownloaded(_ downloaded: Bool) {
        isDownloaded = downloaded
        downloadArrowView.isHidden = downloaded
        playPauseAnimationView.isHidden = !downloaded
    }

    /// Show download progress ring overlaying the play button.
    func showDownloadProgress(_ progress: CGFloat) {
        downloadRingView.show()
        downloadRingView.setProgress(progress)
        downloadArrowView.isHidden = true
        playPauseAnimationView.isHidden = true
    }

    /// Hide download progress ring and show play icon (download complete).
    func hideDownloadProgress() {
        downloadRingView.hide()
        isDownloaded = true
        downloadArrowView.isHidden = true
        playPauseAnimationView.isHidden = false
    }

    /// Cancel download: hide ring, return to "needs download" state (↓ arrow).
    func cancelDownloadProgress() {
        downloadRingView.hide()
        isDownloaded = false
        downloadArrowView.isHidden = false
        playPauseAnimationView.isHidden = true
    }

    // MARK: - Play Action

    @objc private func playTapped() {
        onPlayTapped?()
    }

    /// Update play button icon and waveform progress from VoiceMessagePlayer state.
    func updatePlaybackState(isPlaying: Bool, progress: CGFloat) {
        // Don't override progress while user is scrubbing
        if !waveformView.isScrubbing {
            waveformView.progress = progress
        }
        waveformView.enableScrubbing = isPlaying

        let shouldShowPause = isPlaying
        guard shouldShowPause != isShowingPause else { return }
        isShowingPause = shouldShowPause

        if playPauseAnimationView.animation != nil {
            playButton.setImage(nil, for: .normal)
            if shouldShowPause {
                // play → pause (Telegram: frames 0→41)
                playPauseAnimationView.play(fromFrame: 0, toFrame: 41, loopMode: .playOnce)
            } else {
                // pause → play (Telegram: frames 41→83)
                playPauseAnimationView.play(fromFrame: 41, toFrame: 83, loopMode: .playOnce)
            }
        } else {
            // Fallback: SF Symbols
            let config = UIImage.SymbolConfiguration(pointSize: 16, weight: .bold)
            let name = shouldShowPause ? "pause.fill" : "play.fill"
            playButton.setImage(UIImage(systemName: name, withConfiguration: config), for: .normal)
        }
    }

    /// Updates duration label to show elapsed time during playback, total when stopped.
    func updateDurationDuringPlayback(currentTime: TimeInterval, totalDuration: TimeInterval, isPlaying: Bool) {
        if isPlaying && currentTime > 0 {
            durationLabel.text = Self.formatDuration(currentTime)
        } else {
            durationLabel.text = Self.formatDuration(self.totalDuration)
        }
    }

    private static func formatDuration(_ time: TimeInterval) -> String {
        let totalSeconds = Int(time)
        let minutes = totalSeconds / 60
        let seconds = totalSeconds % 60
        return String(format: "%d:%02d", minutes, seconds)
    }

    // MARK: - Waveform Decoding

    /// Decode waveform from preview string.
    /// Format: comma-separated float values or base64-encoded 5-bit bitstream.
    static func decodeWaveform(from preview: String) -> [Float] {
        guard !preview.isEmpty else { return [] }

        // Try comma-separated floats first
        if preview.contains(",") || preview.contains(".") {
            let parts = preview.split(separator: ",")
            let values = parts.compactMap { Float($0.trimmingCharacters(in: .whitespaces)) }
            if !values.isEmpty { return values }
        }

        // Try base64-encoded 5-bit bitstream (Telegram format)
        guard let data = Data(base64Encoded: preview), !data.isEmpty else {
            return []
        }
        return decode5BitWaveform(data)
    }

    /// Decode 5-bit packed waveform data (Telegram AudioWaveform format).
    /// Each sample is 5 bits (0-31), normalized to 0.0-1.0.
    private static func decode5BitWaveform(_ data: Data) -> [Float] {
        let bitCount = data.count * 8
        let sampleCount = bitCount / 5
        guard sampleCount > 0 else { return [] }

        var samples = [Float](repeating: 0, count: sampleCount)
        let bytes = [UInt8](data)

        for i in 0..<sampleCount {
            let bitOffset = i * 5
            let byteIndex = bitOffset / 8
            let bitIndex = bitOffset % 8

            var value: UInt8 = 0
            if bitIndex + 5 <= 8 {
                value = (bytes[byteIndex] >> (8 - bitIndex - 5)) & 0x1F
            } else {
                let bitsFromFirst = 8 - bitIndex
                let bitsFromSecond = 5 - bitsFromFirst
                value = (bytes[byteIndex] & ((1 << bitsFromFirst) - 1)) << bitsFromSecond
                if byteIndex + 1 < bytes.count {
                    value |= bytes[byteIndex + 1] >> (8 - bitsFromSecond)
                }
            }

            samples[i] = Float(value) / 31.0
        }

        return samples
    }

    // MARK: - Waveform Encoding

    /// Encode waveform samples to comma-separated floats (Desktop parity).
    /// Desktop DialogInput.tsx:217 sends `interpolateCompressWaves(35).join(",")`.
    /// Desktop MessageVoice.tsx parses with `split(",").map(parseFloat)`.
    static func encodeWaveform(_ samples: [Float]) -> String {
        guard !samples.isEmpty else { return "" }

        // Resample to 35 bars (Desktop standard: interpolateCompressWaves(35))
        let targetCount = min(35, samples.count)
        let step = Float(samples.count) / Float(targetCount)
        var resampled = [Float](repeating: 0, count: targetCount)
        for i in 0..<targetCount {
            let start = Int(Float(i) * step)
            let end = min(samples.count, Int(Float(i + 1) * step))
            guard start < end else { continue }
            resampled[i] = samples[start..<end].max() ?? 0
        }

        // Comma-separated floats (Desktop parity)
        return resampled.map { String(format: "%.2f", $0) }.joined(separator: ",")
    }
}
