import UIKit

// MARK: - In-App Notification Banner (UIKit only)

/// Telegram-style in-app notification banner implemented fully in UIKit.
/// No SwiftUI bridge is used in this view.
final class InAppBannerView: UIView {

    private enum Metrics {
        // Telegram in-app notifications currently use compact layout.
        static let panelHeight: CGFloat = 64
        static let cornerRadius: CGFloat = 24
        static let avatarSize: CGFloat = 40
        static let avatarLeading: CGFloat = 12
        // Telegram compact reference: leftInset = imageSize + imageSpacing = 40 + 22 = 62.
        static let textLeading: CGFloat = 62
        static let trailingInset: CGFloat = 10
        static let verticalTextSpacing: CGFloat = 1
        static let topInset: CGFloat = 6
    }

    static let preferredHeight: CGFloat = Metrics.panelHeight
    static let preferredTopInset: CGFloat = Metrics.topInset

    private let glassView = TelegramGlassUIView(frame: .zero)

    private let avatarBackgroundView = UIView()
    private let avatarTintOverlayView = UIView()
    private let avatarImageView = UIImageView()
    private let avatarInitialsLabel = UILabel()
    private let groupIconView = UIImageView()

    private let titleLabel = UILabel()
    private let messageLabel = UILabel()

    private var senderName: String = ""
    private var messagePreview: String = ""
    private var senderKey: String = ""
    private var isGroup: Bool = false

    private var onTap: (() -> Void)?
    private var onDismiss: (() -> Void)?
    private var onExpand: (() -> Void)?
    private var onDragBegan: (() -> Void)?
    private var onDragEndedWithoutAction: (() -> Void)?

    private var hapticFeedback: UIImpactFeedbackGenerator?
    private var cancelledTimeout = false
    private var isAnimatingOut = false
    private var isPanning = false

    private var willBeExpanded = false {
        didSet {
            guard willBeExpanded != oldValue else { return }
            if hapticFeedback == nil {
                hapticFeedback = UIImpactFeedbackGenerator(style: .medium)
            }
            hapticFeedback?.impactOccurred()
        }
    }

    override var intrinsicContentSize: CGSize {
        CGSize(width: UIView.noIntrinsicMetric, height: Metrics.panelHeight)
    }

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

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func applyCallbacks(
        onTap: @escaping () -> Void,
        onDismiss: @escaping () -> Void,
        onExpand: @escaping () -> Void,
        onDragBegan: @escaping () -> Void,
        onDragEndedWithoutAction: @escaping () -> Void
    ) {
        self.onTap = onTap
        self.onDismiss = onDismiss
        self.onExpand = onExpand
        self.onDragBegan = onDragBegan
        self.onDragEndedWithoutAction = onDragEndedWithoutAction
    }

    func configure(senderName: String, messagePreview: String, senderKey: String, isGroup: Bool) {
        let senderChanged = self.senderName != senderName || self.senderKey != senderKey || self.isGroup != isGroup
        let messageChanged = self.messagePreview != messagePreview

        self.senderName = senderName
        self.messagePreview = messagePreview
        self.senderKey = senderKey
        self.isGroup = isGroup

        if senderChanged {
            titleLabel.text = senderName
            updateAvatar()
        }

        if messageChanged {
            messageLabel.text = Self.singleLinePreviewText(from: messagePreview)
        }

        resetTransientTransformState()
        setNeedsLayout()
    }

    func animateIn() {
        guard !isAnimatingOut else { return }
        let startY = -(bounds.height + 24.0)
        transform = CGAffineTransform(translationX: 0, y: startY)
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            options: [.curveEaseInOut, .beginFromCurrentState, .allowUserInteraction]
        ) {
            self.transform = .identity
        }
    }

    func animateOut(completion: @escaping () -> Void) {
        guard !isAnimatingOut else {
            completion()
            return
        }
        isAnimatingOut = true

        let targetY = -(bounds.height + 24.0)
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            options: [.curveEaseOut, .beginFromCurrentState, .allowUserInteraction]
        ) {
            self.transform = CGAffineTransform(translationX: 0, y: targetY)
        } completion: { _ in
            self.isAnimatingOut = false
            completion()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        guard bounds.width > 0, bounds.height > 0 else { return }

        glassView.frame = bounds

        let avatarY = floor((bounds.height - Metrics.avatarSize) / 2.0)
        let avatarFrame = CGRect(
            x: Metrics.avatarLeading,
            y: avatarY,
            width: Metrics.avatarSize,
            height: Metrics.avatarSize
        )

        avatarBackgroundView.frame = avatarFrame
        avatarTintOverlayView.frame = avatarBackgroundView.bounds
        avatarImageView.frame = avatarFrame
        avatarInitialsLabel.frame = avatarFrame
        groupIconView.frame = avatarFrame

        let leftInset = Metrics.textLeading
        let textWidth = max(0, bounds.width - leftInset - Metrics.trailingInset)

        let titleSize = titleLabel.sizeThatFits(
            CGSize(width: textWidth, height: CGFloat.greatestFiniteMagnitude)
        )
        let titleHeight = ceil(min(titleSize.height, titleLabel.font.lineHeight))

        let rawMessageSize = messageLabel.sizeThatFits(
            CGSize(width: textWidth, height: CGFloat.greatestFiniteMagnitude)
        )
        let maxMessageHeight = ceil(messageLabel.font.lineHeight)
        let messageHeight = ceil(min(rawMessageSize.height, maxMessageHeight))

        let totalTextHeight = titleHeight + Metrics.verticalTextSpacing + messageHeight
        let titleY = floor((bounds.height - totalTextHeight) / 2.0)

        titleLabel.frame = CGRect(x: leftInset, y: titleY, width: textWidth, height: titleHeight)
        messageLabel.frame = CGRect(
            x: leftInset,
            y: titleLabel.frame.maxY + Metrics.verticalTextSpacing,
            width: textWidth,
            height: messageHeight
        )
    }

    override func didMoveToWindow() {
        super.didMoveToWindow()
        if window == nil {
            resetTransientTransformState()
        }
    }

    // MARK: - Setup

    private func setupView() {
        backgroundColor = .clear
        isOpaque = false
        clipsToBounds = true
        layer.cornerCurve = .continuous
        layer.cornerRadius = Metrics.cornerRadius

        glassView.backgroundColor = .clear
        glassView.fixedCornerRadius = Metrics.cornerRadius
        glassView.isUserInteractionEnabled = false
        addSubview(glassView)

        avatarBackgroundView.clipsToBounds = true
        avatarBackgroundView.layer.cornerRadius = Metrics.avatarSize / 2.0
        addSubview(avatarBackgroundView)

        avatarTintOverlayView.clipsToBounds = true
        avatarTintOverlayView.layer.cornerRadius = Metrics.avatarSize / 2.0
        avatarBackgroundView.addSubview(avatarTintOverlayView)

        avatarImageView.contentMode = .scaleAspectFill
        avatarImageView.clipsToBounds = true
        avatarImageView.layer.cornerRadius = Metrics.avatarSize / 2.0
        addSubview(avatarImageView)

        avatarInitialsLabel.textAlignment = .center
        avatarInitialsLabel.font = .systemFont(ofSize: 20, weight: .bold)
        addSubview(avatarInitialsLabel)

        groupIconView.contentMode = .center
        addSubview(groupIconView)

        titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
        titleLabel.numberOfLines = 1
        titleLabel.lineBreakMode = .byTruncatingTail
        addSubview(titleLabel)

        messageLabel.font = .systemFont(ofSize: 15, weight: .regular)
        messageLabel.numberOfLines = 1
        messageLabel.lineBreakMode = .byTruncatingTail
        addSubview(messageLabel)

        applyTextColors()

        if #available(iOS 17.0, *) {
            registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, _) in
                self.applyTextColors()
                self.updateAvatar()
            }
        }

        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
        tapRecognizer.cancelsTouchesInView = false
        let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        panRecognizer.delaysTouchesBegan = false
        panRecognizer.delaysTouchesEnded = false
        panRecognizer.cancelsTouchesInView = false

        // Prevent tap from firing after tiny drags.
        tapRecognizer.require(toFail: panRecognizer)

        addGestureRecognizer(tapRecognizer)
        addGestureRecognizer(panRecognizer)
    }

    private func applyTextColors() {
        let isDark = traitCollection.userInterfaceStyle == .dark
        let color = isDark ? UIColor.white : UIColor.black
        titleLabel.textColor = color
        messageLabel.textColor = color
    }

    private static func singleLinePreviewText(from text: String) -> String {
        text
            .replacingOccurrences(of: "\n", with: " ")
            .replacingOccurrences(of: "\r", with: " ")
    }

    private func updateAvatar() {
        let avatarIndex = RosettaColors.avatarColorIndex(for: senderName, publicKey: senderKey)
        let tintColor = RosettaColors.avatarColor(for: avatarIndex)
        let textColor = RosettaColors.avatarTextColor(for: avatarIndex)

        if isGroup {
            groupIconView.isHidden = true

            // Try loading group photo avatar first (same as 1:1 chats)
            if let image = AvatarRepository.shared.loadAvatar(publicKey: senderKey) {
                avatarImageView.image = image
                avatarImageView.isHidden = false
                avatarBackgroundView.isHidden = true
                avatarInitialsLabel.isHidden = true
                avatarTintOverlayView.isHidden = true
            } else {
                avatarImageView.isHidden = true
                avatarBackgroundView.isHidden = false
                avatarTintOverlayView.isHidden = false
                avatarInitialsLabel.isHidden = false

                let isDark = traitCollection.userInterfaceStyle == .dark
                avatarBackgroundView.backgroundColor = isDark
                    ? UIColor(red: 0x1A / 255.0, green: 0x1B / 255.0, blue: 0x1E / 255.0, alpha: 1.0)
                    : UIColor(red: 0xF1 / 255.0, green: 0xF3 / 255.0, blue: 0xF5 / 255.0, alpha: 1.0)
                avatarTintOverlayView.backgroundColor = tintColor.withAlphaComponent(0.15)
                avatarInitialsLabel.textColor = textColor
                avatarInitialsLabel.text = RosettaColors.groupInitial(name: senderName, publicKey: senderKey)
            }
            return
        }

        groupIconView.isHidden = true

        if let image = AvatarRepository.shared.loadAvatar(publicKey: senderKey) {
            avatarImageView.image = image
            avatarImageView.isHidden = false
            avatarBackgroundView.isHidden = true
            avatarInitialsLabel.isHidden = true
            avatarTintOverlayView.isHidden = true
        } else {
            avatarImageView.isHidden = true
            avatarBackgroundView.isHidden = false
            avatarInitialsLabel.isHidden = false
            avatarTintOverlayView.isHidden = false

            let isDark = traitCollection.userInterfaceStyle == .dark
            avatarBackgroundView.backgroundColor = isDark
                ? UIColor(red: 0x1A / 255.0, green: 0x1B / 255.0, blue: 0x1E / 255.0, alpha: 1.0)
                : UIColor(red: 0xF1 / 255.0, green: 0xF3 / 255.0, blue: 0xF5 / 255.0, alpha: 1.0)
            avatarTintOverlayView.backgroundColor = tintColor.withAlphaComponent(0.15)
            avatarInitialsLabel.textColor = textColor
            avatarInitialsLabel.text = RosettaColors.initials(name: senderName, publicKey: senderKey)
        }
    }

    // MARK: - Gestures

    @objc
    private func handleTapGesture(_ recognizer: UITapGestureRecognizer) {
        guard recognizer.state == .ended, !isPanning else { return }
        emit(onTap)
    }

    @objc
    private func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            cancelledTimeout = false
            isPanning = false
            willBeExpanded = false
            hapticFeedback = nil

        case .changed:
            if isAnimatingOut { return }
            let translation = recognizer.translation(in: self)
            if !isPanning && abs(translation.y) > 4.0 {
                isPanning = true
            }
            let displayedOffset = displayedOffset(for: translation.y)

            if abs(translation.y) > 1.0 {
                if hapticFeedback == nil {
                    hapticFeedback = UIImpactFeedbackGenerator(style: .medium)
                }
                hapticFeedback?.prepare()
            }

            setInteractiveOffset(displayedOffset)

            if !cancelledTimeout && abs(translation.y) > 4.0 {
                cancelledTimeout = true
                emit(onDragBegan)
            }

            let expand = displayedOffset > 20.0
            if willBeExpanded != expand {
                willBeExpanded = expand
            }

        case .ended:
            isPanning = false
            let translation = recognizer.translation(in: self)
            let velocity = recognizer.velocity(in: self)
            let displayedOffset = displayedOffset(for: translation.y)

            if displayedOffset > 20.0 || velocity.y > 300.0 {
                if !cancelledTimeout {
                    cancelledTimeout = true
                    emit(onDragBegan)
                }
                animateOffsetToOrigin(completion: { [weak self] in
                    self?.emit(self?.onExpand)
                })
            } else if displayedOffset < -5.0 || velocity.y < -200.0 {
                animateOut(completion: { [weak self] in
                    self?.emit(self?.onDismiss)
                })
            } else {
                if cancelledTimeout {
                    cancelledTimeout = false
                    emit(onDragEndedWithoutAction)
                }
                animateOffsetToOrigin(completion: nil)
            }

            willBeExpanded = false
            hapticFeedback = nil

        case .cancelled, .failed:
            isPanning = false
            if cancelledTimeout {
                cancelledTimeout = false
                emit(onDragEndedWithoutAction)
            }
            animateOffsetToOrigin(completion: nil)
            willBeExpanded = false
            hapticFeedback = nil

        default:
            break
        }
    }

    // MARK: - Animation helpers

    private func animateOffsetToOrigin(completion: (() -> Void)?) {
        guard !isAnimatingOut else { return }
        guard abs(transform.ty) > 0.001 else {
            completion?()
            return
        }

        UIView.animate(
            withDuration: 0.3,
            delay: 0,
            options: [.curveEaseInOut, .beginFromCurrentState, .allowUserInteraction]
        ) {
            self.transform = .identity
        } completion: { _ in
            completion?()
        }
    }

    private func setInteractiveOffset(_ offsetY: CGFloat) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        transform = CGAffineTransform(translationX: 0, y: offsetY)
        CATransaction.commit()
    }

    private func displayedOffset(for translationY: CGFloat) -> CGFloat {
        if translationY < 0 {
            return translationY
        }
        return (1.0 - (1.0 / (((translationY) * 0.55 / 50.0) + 1.0))) * 50.0
    }

    private func resetTransientTransformState() {
        isAnimatingOut = false
        layer.removeAllAnimations()
        transform = .identity
    }

    private func emit(_ callback: (() -> Void)?) {
        guard let callback else { return }
        if Thread.isMainThread {
            callback()
        } else {
            DispatchQueue.main.async(execute: callback)
        }
    }
}
