import UIKit
import Theme
import Models
import Shared
import Combine
import XXModels
import Voxophone
import ChatLayout
import DrawerFeature
import DifferenceKit
import ChatInputFeature
import DependencyInjection

typealias OutgoingGroupTextCell = CollectionCell<FlexibleSpace, StackMessageView>
typealias IncomingGroupTextCell = CollectionCell<StackMessageView, FlexibleSpace>
typealias OutgoingGroupReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView>
typealias IncomingGroupReplyCell = CollectionCell<ReplyStackMessageView, FlexibleSpace>
typealias OutgoingFailedGroupTextCell = CollectionCell<FlexibleSpace, StackMessageView>
typealias OutgoingFailedGroupReplyCell = CollectionCell<FlexibleSpace, ReplyStackMessageView>

public final class GroupChatController: UIViewController {
    @Dependency private var coordinator: ChatCoordinating
    @Dependency private var statusBarController: StatusBarStyleControlling

    private let members: MembersController
    private var collectionView: UICollectionView!
    lazy private var header = GroupHeaderView()
    private let inputComponent: ChatInputView

    private let chatLayout = ChatLayout()
    private var animator: ManualAnimator?
    private let viewModel: GroupChatViewModel
    private let layoutDelegate = LayoutDelegate()
    private var cancellables = Set<AnyCancellable>()
    private var drawerCancellables = Set<AnyCancellable>()
    private var sections = [ArraySection<ChatSection, Message>]()
    private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>()

    public override var canBecomeFirstResponder: Bool { true }
    public override var inputAccessoryView: UIView? { inputComponent }

    public init(_ info: GroupInfo) {
        let viewModel = GroupChatViewModel(info)
        self.viewModel = viewModel
        self.members = .init(with: info.members)

        self.inputComponent = ChatInputView(store: .init(
            initialState: .init(canAddAttachments: false),
            reducer: chatInputReducer,
            environment: .init(
                voxophone: try! DependencyInjection.Container.shared.resolve() as Voxophone,
                sendAudio: { _ in },
                didTapCamera: {},
                didTapLibrary: {},
                sendText: { viewModel.send($0) },
                didTapAbortReply: { viewModel.abortReply() },
                didTapMicrophone: { false }
            )
        ))

        super.init(nibName: nil, bundle: nil)

        let memberList = info.members.map {
            Member(
                title: ($0.nickname ?? $0.username) ?? "Fetching username...",
                photo: $0.photo
            )
        }

        header.setup(title: info.group.name, memberList: memberList)
    }

    public required init?(coder: NSCoder) { nil }

    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        statusBarController.style.send(.darkContent)
        navigationController?.navigationBar.customize(
            backgroundColor: Asset.neutralWhite.color,
            shadowColor: Asset.neutralDisabled.color
        )
    }

    public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        collectionView.collectionViewLayout.invalidateLayout()
        becomeFirstResponder()
    }

    private var isFirstAppearance = true

    public override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        if isFirstAppearance {
            isFirstAppearance = false
            let insets = UIEdgeInsets(
                top: 0,
                left: 0,
                bottom: inputComponent.bounds.height - view.safeAreaInsets.bottom,
                right: 0
            )
            collectionView.contentInset = insets
            collectionView.scrollIndicatorInsets = insets
        }
    }

    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        viewModel.readAll()
    }

    public override func viewDidLoad() {
        super.viewDidLoad()

        setupNavigationBar()
        setupCollectionView()
        setupInputController()
        setupBindings()

        KeyboardListener.shared.add(delegate: self)
    }

    private func setupNavigationBar() {
        let more = UIButton()
        more.setImage(Asset.chatMore.image, for: .normal)
        more.addTarget(self, action: #selector(didTapDots), for: .touchUpInside)

        navigationItem.titleView = header
        navigationItem.rightBarButtonItem = UIBarButtonItem(customView: more)
    }

    private func setupCollectionView() {
        chatLayout.configure(layoutDelegate)
        collectionView = .init(on: view, with: chatLayout)
        collectionView.register(IncomingGroupTextCell.self)
        collectionView.register(OutgoingGroupTextCell.self)
        collectionView.register(IncomingGroupReplyCell.self)
        collectionView.register(OutgoingGroupReplyCell.self)
        collectionView.register(OutgoingFailedGroupTextCell.self)
        collectionView.register(OutgoingFailedGroupReplyCell.self)
        collectionView.registerSectionHeader(SectionHeaderView.self)
        collectionView.dataSource = self
        collectionView.delegate = self
    }

    private func setupInputController() {
        inputComponent.setMaxHeight { [weak self] in
            guard let self = self else { return 150 }

            let maxHeight = self.collectionView.frame.height
            - self.collectionView.adjustedContentInset.top
            - self.collectionView.adjustedContentInset.bottom
            + self.inputComponent.bounds.height

            return maxHeight * 0.9
        }

        viewModel.replyPublisher
            .receive(on: DispatchQueue.main)
            .sink { [unowned self] senderTitle, messageText in
                inputComponent.setupReply(message: messageText, sender: senderTitle)
            }
            .store(in: &cancellables)
    }

    private func setupBindings() {
        viewModel.routesPublisher
            .receive(on: DispatchQueue.main)
            .sink { [unowned self] in
                switch $0 {
                case .waitingRound:
                    coordinator.toDrawer(makeWaitingRoundDrawer(), from: self)
                case .webview(let urlString):
                    coordinator.toWebview(with: urlString, from: self)
                }
            }.store(in: &cancellables)

        viewModel.messages
            .receive(on: DispatchQueue.main)
            .sink { [unowned self] sections in
                func process() {
                    let changeSet = StagedChangeset(source: self.sections, target: sections).flattenIfPossible()
                    collectionView.reload(
                        using: changeSet,
                        interrupt: { changeSet in
                            guard !self.sections.isEmpty else { return true }
                            return false
                        }, onInterruptedReload: {
                            guard let lastSection = self.sections.last else { return }
                            let positionSnapshot = ChatLayoutPositionSnapshot(
                                indexPath: IndexPath(
                                    item: lastSection.elements.count - 1,
                                    section: self.sections.count - 1
                                ),
                                kind: .cell,
                                edge: .bottom
                            )

                            self.collectionView.reloadData()
                            self.chatLayout.restoreContentOffset(with: positionSnapshot)
                        },
                        completion: nil,
                        setData: { self.sections = $0 }
                    )
                }

                guard currentInterfaceActions.options.isEmpty else {
                    let reaction = SetActor<Set<InterfaceActions>, ReactionTypes>.Reaction(
                        type: .delayedUpdate,
                        action: .onEmpty,
                        executionType: .once,
                        actionBlock: { [weak self] in
                            guard let _ = self else { return }
                            process()
                        }
                    )

                    currentInterfaceActions.add(reaction: reaction)
                    return
                }

                process()
            }
            .store(in: &cancellables)
    }

    @objc private func didTapDots() {
        coordinator.toMembersList(members, from: self)
    }

    private func makeWaitingRoundDrawer() -> UIViewController {
        let text = DrawerText(
            font: Fonts.Mulish.semiBold.font(size: 14.0),
            text: Localized.Chat.RoundDrawer.title,
            color: Asset.neutralWeak.color,
            lineHeightMultiple: 1.35,
            spacingAfter: 25
        )

        let button = DrawerCapsuleButton(model: .init(
            title: Localized.Chat.RoundDrawer.action,
            style: .brandColored
        ))

        let drawer = DrawerController(with: [text, button])

        button.action
            .receive(on: DispatchQueue.main)
            .sink { [weak drawer] in
                drawer?.dismiss(animated: true) { [weak self] in
                    guard let self = self else { return }
                    self.drawerCancellables.removeAll()
                }
            }.store(in: &drawerCancellables)

        return drawer
    }

    func scrollToBottom(completion: (() -> Void)? = nil) {
        let contentOffsetAtBottom = CGPoint(
            x: collectionView.contentOffset.x,
            y: chatLayout.collectionViewContentSize.height
            - collectionView.frame.height + collectionView.adjustedContentInset.bottom
        )

        guard contentOffsetAtBottom.y > collectionView.contentOffset.y else { completion?(); return }

        let initialOffset = collectionView.contentOffset.y
        let delta = contentOffsetAtBottom.y - initialOffset

        if abs(delta) > chatLayout.visibleBounds.height {
            animator = ManualAnimator()
            animator?.animate(duration: TimeInterval(0.25), curve: .easeInOut) { [weak self] percentage in
                guard let self = self else { return }

                self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: initialOffset + (delta * percentage))
                if percentage == 1.0 {
                    self.animator = nil
                    let positionSnapshot = ChatLayoutPositionSnapshot(indexPath: IndexPath(item: 0, section: 0), kind: .footer, edge: .bottom)
                    self.chatLayout.restoreContentOffset(with: positionSnapshot)
                    self.currentInterfaceActions.options.remove(.scrollingToBottom)
                    completion?()
                }
            }
        } else {
            currentInterfaceActions.options.insert(.scrollingToBottom)
            UIView.animate(withDuration: 0.25, animations: {
                self.collectionView.setContentOffset(contentOffsetAtBottom, animated: true)
            }, completion: { [weak self] _ in
                self?.currentInterfaceActions.options.remove(.scrollingToBottom)
                completion?()
            })
        }
    }
}

extension GroupChatController: UICollectionViewDataSource {
    public func numberOfSections(in collectionView: UICollectionView) -> Int {
        sections.count
    }

    public func collectionView(_ collectionView: UICollectionView,
                               viewForSupplementaryElementOfKind kind: String,
                               at indexPath: IndexPath) -> UICollectionReusableView {
        let sectionHeader: SectionHeaderView = collectionView.dequeueSupplementaryView(forIndexPath: indexPath)
        sectionHeader.title.text = sections[indexPath.section].model.date.asDayOfMonth()
        return sectionHeader
    }

    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        sections[section].elements.count
    }

    public func collectionView(
        _ collectionView: UICollectionView,
        cellForItemAt indexPath: IndexPath
    ) -> UICollectionViewCell {

        var item = sections[indexPath.section].elements[indexPath.item]
        let canReply: () -> Bool = {
            (item.status == .sent || item.status == .received) && item.networkId != nil
        }

        let performReply: () -> Void = { [weak self] in
            self?.viewModel.didRequestReply(item)
        }

        let name: (Data) -> String = viewModel.getName(from:)
        let showRound: (String?) -> Void = viewModel.showRoundFrom(_:)
        let replyContent: (Data) -> (String, String) = viewModel.getReplyContent(for:)

        var isSenderBanned = false

        if let database = try? DependencyInjection.Container.shared.resolve() as Database,
           let sender = try? database.fetchContacts(.init(id: [item.senderId])).first {
            isSenderBanned = sender.isBanned
        }

        if item.status == .received {
            guard isSenderBanned == false else {
                item.text = "This user has been banned"

                let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
                Bubbler.buildGroup(
                    bubble: cell.leftView,
                    with: item,
                    with: "Banned user"
                )

                cell.canReply = false
                cell.performReply = {}
                cell.leftView.didTapShowRound = {}

                return cell
            }

            if let replyMessageId = item.replyMessageId {
                let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReplyGroup(
                    bubble: cell.leftView,
                    with: item,
                    reply: replyContent(replyMessageId),
                    sender: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply
                cell.leftView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            } else {
                let cell: IncomingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
                Bubbler.buildGroup(
                    bubble: cell.leftView,
                    with: item,
                    with: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply
                cell.leftView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            }
        } else if item.status == .sendingFailed {
            if let replyMessageId = item.replyMessageId {
                let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReplyGroup(
                    bubble: cell.rightView,
                    with: item,
                    reply: replyContent(replyMessageId),
                    sender: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply

                return cell
            } else {
                let cell: OutgoingFailedGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildGroup(
                    bubble: cell.rightView,
                    with: item,
                    with: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply

                return cell
            }
        } else {
            if let replyMessageId = item.replyMessageId {
                let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReplyGroup(
                    bubble: cell.rightView,
                    with: item,
                    reply: replyContent(replyMessageId),
                    sender: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply
                cell.rightView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            } else {
                let cell: OutgoingGroupTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildGroup(
                    bubble: cell.rightView,
                    with: item,
                    with: name(item.senderId)
                )

                cell.canReply = canReply()
                cell.performReply = performReply
                cell.rightView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            }
        }
    }
}

extension GroupChatController: KeyboardListenerDelegate {
    fileprivate var isUserInitiatedScrolling: Bool {
        return collectionView.isDragging || collectionView.isDecelerating
    }

    func keyboardWillChangeFrame(info: KeyboardInfo) {
        let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first

        guard !currentInterfaceActions.options.contains(.changingFrameSize),
              collectionView.contentInsetAdjustmentBehavior != .never,
              let keyboardFrame = keyWindow?.convert(info.frameEnd, to: view),
              collectionView.convert(collectionView.bounds, to: keyWindow).maxY > info.frameEnd.minY else {
                  return
              }
        currentInterfaceActions.options.insert(.changingKeyboardFrame)
        let newBottomInset = collectionView.frame.minY + collectionView.frame.size.height - keyboardFrame.minY - collectionView.safeAreaInsets.bottom
        if newBottomInset > 0,
           collectionView.contentInset.bottom != newBottomInset {
            let positionSnapshot = chatLayout.getContentOffsetSnapshot(from: .bottom)

            currentInterfaceActions.options.insert(.changingContentInsets)
            UIView.animate(withDuration: info.animationDuration, animations: {
                self.collectionView.performBatchUpdates({
                    self.collectionView.contentInset.bottom = newBottomInset
                    self.collectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
                }, completion: nil)

                if let positionSnapshot = positionSnapshot, !self.isUserInitiatedScrolling {
                    self.chatLayout.restoreContentOffset(with: positionSnapshot)
                }
            }, completion: { _ in
                self.currentInterfaceActions.options.remove(.changingContentInsets)
            })
        }
    }

    func keyboardDidChangeFrame(info: KeyboardInfo) {
        guard currentInterfaceActions.options.contains(.changingKeyboardFrame) else { return }
        currentInterfaceActions.options.remove(.changingKeyboardFrame)
    }
}

extension GroupChatController: UICollectionViewDelegate {
    private func makeTargetedPreview(for configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
        guard let identifier = configuration.identifier as? String,
              let first = identifier.components(separatedBy: "|").first,
              let last = identifier.components(separatedBy: "|").last,
              let item = Int(first), let section = Int(last),
              let cell = collectionView.cellForItem(at: IndexPath(item: item, section: section)) else {
                  return nil
              }

        let parameters = UIPreviewParameters()
        parameters.backgroundColor = .clear

        if sections[section].elements[item].status == .received {
            var leftView: UIView!

            if let cell = cell as? IncomingGroupReplyCell {
                leftView = cell.leftView
            } else if let cell = cell as? IncomingGroupTextCell {
                leftView = cell.leftView
            }

            parameters.visiblePath = UIBezierPath(roundedRect: leftView.bounds, cornerRadius: 13)
            return UITargetedPreview(view: leftView, parameters: parameters)
        }

        var rightView: UIView!

        if let cell = cell as? OutgoingGroupTextCell {
            rightView = cell.rightView
        } else if let cell = cell as? OutgoingGroupReplyCell {
            rightView = cell.rightView
        }

        parameters.visiblePath = UIBezierPath(roundedRect: rightView.bounds, cornerRadius: 13)
        return UITargetedPreview(view: rightView, parameters: parameters)
    }

    public func collectionView(
        _ collectionView: UICollectionView,
        previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
    ) -> UITargetedPreview? {
        makeTargetedPreview(for: configuration)
    }

    public func collectionView(
        _ collectionView: UICollectionView,
        previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
    ) -> UITargetedPreview? {
        makeTargetedPreview(for: configuration)
    }

    public func collectionView(
        _ collectionView: UICollectionView,
        contextMenuConfigurationForItemAt indexPath: IndexPath,
        point: CGPoint
    ) -> UIContextMenuConfiguration? {
        UIContextMenuConfiguration(
            identifier: "\(indexPath.item)|\(indexPath.section)" as NSCopying,
            previewProvider: nil
        ) { [weak self] suggestedActions in

            guard let self = self else { return nil }

            let item = self.sections[indexPath.section].elements[indexPath.item]

            let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in
                UIPasteboard.general.string = item.text
            }

            let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in
                self?.viewModel.didRequestReply(item)
            }

            let delete = UIAction(title: Localized.Chat.BubbleMenu.delete, state: .off) { [weak self] _ in
                self?.viewModel.didRequestDelete([item])
            }

            let retry = UIAction(title: Localized.Chat.BubbleMenu.retry, state: .off) { [weak self] _ in
                self?.viewModel.retry(item)
            }

            let menu: UIMenu

            if item.status == .sendingFailed {
                menu = UIMenu(title: "", children: [copy, retry, delete])
            } else if item.status == .sending {
                menu = UIMenu(title: "", children: [copy])
            } else {
                menu = UIMenu(title: "", children: [copy, reply, delete])
            }

            return menu
        }
    }
}