//
//  ChatViewController.swift
//  LanguageModelChatUI
//

import Combine
import UIKit

/// A complete chat view controller that provides message display and user input.
///
/// Usage:
///
///     let vc = ChatViewController(conversationID: "conv-1", sessionConfiguration: configuration)
///     present(vc, animated: true)
///
open class ChatViewController: UIViewController {
    private enum Layout {
        static let topBarHorizontalInset: CGFloat = 16
        static let topBarTopSpacing: CGFloat = 12
        static let topBarBottomSpacing: CGFloat = 14
        static let topBarTouchSize: CGFloat = 44
        static let topBarTitleSpacing: CGFloat = 16
        static let topBarDividerHeight: CGFloat = 1
        static let topBarAvatarSize: CGFloat = 24
    }

    public let conversationID: String
    public let conversationModels: ConversationSession.Models
    public let sessionConfiguration: ConversationSession.Configuration
    public var configuration: Configuration

    /// When `true`, the controller assumes it is embedded inside a
    /// `UINavigationController` and hides its own top bar, instead
    /// placing the title in the navigation bar (always inline/compact)
    /// and moving the menu to a toolbar bar-button item.
    public var prefersNavigationBarManaged: Bool = false {
        didSet {
            guard isViewLoaded else { return }
            applyNavigationBarManagedMode()
        }
    }

    public var inputConfiguration: ChatInputConfiguration {
        get { configuration.input }
        set {
            configuration.input = newValue
            chatInputView.configuration = newValue
        }
    }

    public private(set) var chatInputView = ChatInputView()
    public private(set) var messageListView = MessageListView()

    /// Optional view shown centered in the message area when the conversation is empty.
    /// Set this before `viewDidLoad` or afterwards — it will be added/removed automatically.
    public var emptyStateView: UIView? {
        didSet {
            oldValue?.removeFromSuperview()
            guard isViewLoaded, let emptyStateView else { return }
            view.insertSubview(emptyStateView, aboveSubview: messageListView)
            updateEmptyState()
        }
    }
    /// Called when the user taps a link in a response message.
    public var onLinkTap: ((URL) -> Void)?

    /// Called when the user toggles voice mode from within the input bar.
    public var onVoiceSessionToggle: (() -> Void)?
    /// Called when the user stops voice session from within the input bar.
    public var onVoiceSessionStop: (() -> Void)?
    /// Called when the user taps the conversation list button in the input bar.
    public var onConversationListTap: (() -> Void)?
    /// Called when the user taps the prompts button in the control panel.
    public var onPromptsTap: (() -> Void)?
    /// Whether STT dictation is available (controls mic button visibility in input bar).
    public var sttAvailable: Bool = false {
        didSet {
            guard isViewLoaded else { return }
            chatInputView.sttAvailable = sttAvailable
        }
    }

    /// When true, the camera and voice/mic buttons are hidden in the input bar.
    /// Pair with `singleLineMode` for a pure-text chat surface.
    public var minimalistLayout: Bool = false {
        didSet {
            guard isViewLoaded else { return }
            chatInputView.minimalistLayout = minimalistLayout
        }
    }

    /// When true, the input bar is locked to a single-line height.
    public var singleLineMode: Bool = false {
        didSet {
            guard isViewLoaded else { return }
            chatInputView.singleLineMode = singleLineMode
        }
    }

    /// Called when the user taps the mic button to start STT dictation.
    public var onDictationRequested: (() -> Void)?
    /// Called when the user cancels an in-progress dictation.
    public var onDictationCancelled: (() -> Void)?
    /// Called when the user long-presses the mic button (push-to-talk start).
    public var onPushToTalkBegan: (() -> Void)?
    /// Called when the user releases the mic button (push-to-talk end).
    public var onPushToTalkEnded: (() -> Void)?

    public weak var menuDelegate: ChatViewControllerMenuDelegate? {
        didSet {
            guard isViewLoaded else { return }
            configureNavigationItems()
        }
    }

    private let topBarBackgroundView = UIView()
    private let topBarBlurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
    private let topBarDividerView = SeparatorView()
    private let topBarContentView = UIView()
    private let titleAvatarContainerView = UIView()
    private let titleAvatarView = UIImageView()
    private lazy var menuButton: UIButton = .init(type: .system)
    private let navigationTitleView = ChatTitleView()
    private weak var currentSession: ConversationSession?
    private var resolvedTitleMetadata: ConversationTitleMetadata = .init(title: String.localized("Chat"))
    private lazy var dismissKeyboardTapGesture: UITapGestureRecognizer = {
        let gesture = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTapToDismiss))
        gesture.cancelsTouchesInView = false
        return gesture
    }()

    private var cancellables = Set<AnyCancellable>()
    private var keyboardHeight: CGFloat = 0

    private var draftInputObject: ChatInputContent?

    public init(
        conversationID: String = UUID().uuidString,
        models: ConversationSession.Models = .init(),
        sessionConfiguration: ConversationSession.Configuration = .init(storage: DisposableStorageProvider.shared),
        configuration: Configuration = .init()
    ) {
        self.conversationID = conversationID
        conversationModels = models
        self.sessionConfiguration = sessionConfiguration
        self.configuration = configuration
        super.init(nibName: nil, bundle: nil)
        chatInputView.configuration = self.configuration.input
    }

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

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

        view.backgroundColor = .systemBackground
        topBarBackgroundView.backgroundColor = .clear

        configureTopBarViews()

        view.addSubview(topBarBackgroundView)
        view.addSubview(messageListView)
        view.addSubview(chatInputView)
        messageListView.addGestureRecognizer(dismissKeyboardTapGesture)
        messageListView.theme = configuration.messageTheme
        messageListView.onToolCallTap = { [weak self] toolCall in
            self?.presentToolCallDetail(toolCall)
        }
        messageListView.onLinkTap = { [weak self] url in
            self?.onLinkTap?(url)
        }

        let session = ConversationSessionManager.shared.session(for: conversationID, configuration: sessionConfiguration)
        session.refreshContentsFromDatabase()
        applyConversationModels(conversationModels, to: session)
        currentSession = session
        messageListView.session = session
        chatInputView.delegate = self
        chatInputView.sttAvailable = sttAvailable
        chatInputView.minimalistLayout = minimalistLayout
        chatInputView.singleLineMode = singleLineMode
        chatInputView.bind(conversationID: conversationID)
        if let emptyStateView {
            view.insertSubview(emptyStateView, aboveSubview: messageListView)
        }
        configureNavigationItems()
        bindNavigationTitleUpdates(session: session)
        refreshNavigationTitle()

        setupKeyboardObservation()
        setupInputHeightObservation()

        applyNavigationBarManagedMode()
    }

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


    private func layoutViews() {
        let safeArea = view.safeAreaInsets
        let inputHeight = chatInputView.heightPublisher.value
        let bottomPadding = max(safeArea.bottom, 0)
        let topBarHeight = layoutTopBar(safeAreaTop: safeArea.top)
        let inputExtension = bottomPadding
        let totalInputHeight = inputHeight + inputExtension

        chatInputView.bottomBackgroundExtension = inputExtension

        let inputY = view.bounds.height - totalInputHeight - keyboardHeight
        chatInputView.frame = CGRect(
            x: 0,
            y: max(inputY, safeArea.top),
            width: view.bounds.width,
            height: totalInputHeight
        )

        if prefersNavigationBarManaged {
            // Extend the list view behind the translucent navigation bar.
            // Content is offset via contentSafeAreaInsets so it starts below the bar,
            // but scrolls underneath it for the glass morphism effect.
            messageListView.frame = CGRect(
                x: 0,
                y: 0,
                width: view.bounds.width,
                height: chatInputView.frame.minY
            )
            messageListView.contentSafeAreaInsets = UIEdgeInsets(
                top: safeArea.top,
                left: 0,
                bottom: 0,
                right: 0
            )
        } else {
            messageListView.frame = CGRect(
                x: 0,
                y: topBarHeight,
                width: view.bounds.width,
                height: chatInputView.frame.minY - topBarHeight
            )
            messageListView.contentSafeAreaInsets = .zero
        }

        if let emptyStateView, !emptyStateView.isHidden {
            emptyStateView.frame = messageListView.frame
        }
    }

    @discardableResult
    private func layoutTopBar(safeAreaTop: CGFloat) -> CGFloat {
        if prefersNavigationBarManaged {
            topBarBackgroundView.frame = .zero
            return safeAreaTop
        }
        let contentY = safeAreaTop + Layout.topBarTopSpacing
        let contentHeight = max(Layout.topBarTouchSize, navigationTitleView.intrinsicContentSize.height)
        let iconSide = Layout.topBarAvatarSize

        topBarContentView.frame = CGRect(
            x: 0,
            y: contentY,
            width: view.bounds.width,
            height: contentHeight
        )

        topBarBackgroundView.frame = CGRect(
            x: 0,
            y: 0,
            width: view.bounds.width,
            height: topBarContentView.frame.maxY + Layout.topBarBottomSpacing
        )
        topBarBlurView.frame = topBarBackgroundView.bounds
        topBarDividerView.frame = CGRect(
            x: 0,
            y: topBarBackgroundView.bounds.height - Layout.topBarDividerHeight,
            width: topBarBackgroundView.bounds.width,
            height: Layout.topBarDividerHeight
        )

        let avatarY = (contentHeight - iconSide) / 2
        titleAvatarContainerView.frame = CGRect(
            x: Layout.topBarHorizontalInset,
            y: 0,
            width: Layout.topBarTouchSize,
            height: contentHeight
        )
        titleAvatarView.frame = CGRect(x: 0, y: avatarY, width: iconSide, height: iconSide)
        let menuY = (contentHeight - Layout.topBarTouchSize) / 2
        menuButton.frame = CGRect(
            x: topBarContentView.bounds.width - Layout.topBarHorizontalInset - Layout.topBarTouchSize,
            y: menuY,
            width: Layout.topBarTouchSize,
            height: Layout.topBarTouchSize
        )

        let titleSize = navigationTitleView.intrinsicContentSize
        let leadingReservedWidth = titleAvatarContainerView.frame.maxX + Layout.topBarTitleSpacing
        let trailingReservedWidth = topBarContentView.bounds.width - menuButton.frame.minX + Layout.topBarTitleSpacing
        let symmetricReservedWidth = max(leadingReservedWidth, trailingReservedWidth)
        let availableWidth = max(0, topBarContentView.bounds.width - symmetricReservedWidth * 2)
        let titleWidth = min(titleSize.width, availableWidth)
        let titleHeight = min(contentHeight, titleSize.height)

        navigationTitleView.frame = CGRect(
            x: (topBarContentView.bounds.width - titleWidth) / 2,
            y: (contentHeight - titleHeight) / 2,
            width: titleWidth,
            height: titleHeight
        )

        return topBarBackgroundView.frame.maxY
    }

    private func setupKeyboardObservation() {
        NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
            .sink { [weak self] notification in
                guard let self else { return }
                guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
                let converted = view.convert(frame, from: nil)
                keyboardHeight = max(view.bounds.height - converted.minY - view.safeAreaInsets.bottom, 0)
                Self.animateAlongsideKeyboard(notification: notification) {
                    self.layoutViews()
                }
            }
            .store(in: &cancellables)

        NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
            .sink { [weak self] notification in
                self?.keyboardHeight = 0
                Self.animateAlongsideKeyboard(notification: notification) {
                    self?.layoutViews()
                }
            }
            .store(in: &cancellables)
    }

    private static func animateAlongsideKeyboard(notification: Notification, animations: @escaping () -> Void) {
        let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25
        let curveRaw = (notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt) ?? UInt(UIView.AnimationCurve.easeInOut.rawValue)
        let options = UIView.AnimationOptions(rawValue: curveRaw << 16)
        UIView.animate(withDuration: duration, delay: 0, options: options, animations: animations)
    }

    @objc private func handleBackgroundTapToDismiss() {
        view.endEditing(true)
    }

    private func setupInputHeightObservation() {
        chatInputView.heightPublisher
            .removeDuplicates()
            .sink { [weak self] _ in
                UIView.animate(withDuration: 0.2) {
                    self?.layoutViews()
                }
            }
            .store(in: &cancellables)
    }

    private func configureNavigationItems() {
        if prefersNavigationBarManaged {
            // In managed mode, title and menu live in the navigation bar.
            navigationItem.title = resolvedTitleMetadata.title
            navigationItem.largeTitleDisplayMode = .inline

            if let menu = menuDelegate?.chatViewControllerMenu(self) {
                let barButton = UIBarButtonItem(
                    image: UIImage(systemName: "ellipsis.circle"),
                    menu: menu
                )
                navigationItem.rightBarButtonItem = barButton
            } else {
                navigationItem.rightBarButtonItem = nil
            }

            // Hide the custom top bar menu button since nav bar owns it now.
            menuButton.isHidden = true
        } else {
            navigationItem.titleView = nil
            navigationItem.leftBarButtonItem = nil
            navigationItem.rightBarButtonItem = nil

            if let menu = menuDelegate?.chatViewControllerMenu(self) {
                menuButton.menu = menu
                menuButton.isHidden = false
            } else {
                menuButton.menu = nil
                menuButton.isHidden = true
            }
        }

        if isViewLoaded {
            view.setNeedsLayout()
        }
    }

    private func applyNavigationBarManagedMode() {
        let managed = prefersNavigationBarManaged
        topBarBackgroundView.isHidden = managed
        if managed {
            navigationItem.largeTitleDisplayMode = .inline
            if let nav = navigationController {
                nav.navigationBar.prefersLargeTitles = false
            }
        }
        configureNavigationItems()
        view.setNeedsLayout()
    }

    private func bindNavigationTitleUpdates(session: ConversationSession) {
        session.messagesDidChange
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _, _ in
                self?.refreshNavigationTitle()
                self?.updateEmptyState()
            }
            .store(in: &cancellables)
    }

    private func updateEmptyState() {
        guard let emptyStateView else { return }
        let hasMessages = !(currentSession?.messages.isEmpty ?? true)
        let shouldHide = hasMessages
        guard emptyStateView.isHidden != shouldHide else { return }
        if !shouldHide {
            emptyStateView.isHidden = false
            emptyStateView.alpha = 0
        }
        UIView.animate(withDuration: 0.3) {
            emptyStateView.alpha = shouldHide ? 0 : 1
        } completion: { _ in
            if shouldHide {
                emptyStateView.isHidden = true
            }
        }
    }

    private func resolveTitle(from metadata: ConversationTitleMetadata?) -> String {
        guard currentSession != nil else { return String.localized("Chat") }
        if let storedTitle = metadata?.title {
            let normalized = storedTitle.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !normalized.isEmpty, normalized != "Untitled" else { return String.localized("Chat") }
            return storedTitle
        }
        return String.localized("Chat")
    }

    private func refreshNavigationTitle() {
        let storedTitleMetadata = ConversationTitleMetadata(storageValue: currentSession?.storageProvider.title(for: conversationID))
        let resolvedTitle = resolveTitle(from: storedTitleMetadata)

        resolvedTitleMetadata = .init(title: resolvedTitle, avatar: storedTitleMetadata?.avatar ?? ConversationTitleMetadata.defaultAvatar)
        titleAvatarView.image = resolvedTitleMetadata.avatar.emojiImage(canvasSize: 128)
        navigationTitleView.titleText = resolvedTitleMetadata.title
        if prefersNavigationBarManaged {
            navigationItem.title = resolvedTitleMetadata.title
        }
        if isViewLoaded {
            view.setNeedsLayout()
        }
    }

    private func configureTopBarViews() {
        topBarBackgroundView.clipsToBounds = true
        topBarBlurView.isUserInteractionEnabled = false
        topBarContentView.backgroundColor = .clear

        titleAvatarContainerView.isUserInteractionEnabled = false
        titleAvatarView.contentMode = .scaleAspectFit
        titleAvatarView.image = resolvedTitleMetadata.avatar.emojiImage(canvasSize: 128)

        menuButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
        menuButton.setPreferredSymbolConfiguration(
            UIImage.SymbolConfiguration(pointSize: 14, weight: .semibold),
            forImageIn: .normal
        )
        menuButton.showsMenuAsPrimaryAction = true
        menuButton.tintColor = UIColor.secondaryLabel.withAlphaComponent(0.5)
        menuButton.imageView?.contentMode = .scaleAspectFit
        menuButton.contentHorizontalAlignment = .right
        menuButton.contentVerticalAlignment = .center
        menuButton.isHidden = true

        topBarBackgroundView.addSubview(topBarBlurView)
        topBarBackgroundView.addSubview(topBarDividerView)
        topBarBackgroundView.addSubview(topBarContentView)
        topBarContentView.addSubview(titleAvatarContainerView)
        titleAvatarContainerView.addSubview(titleAvatarView)
        topBarContentView.addSubview(navigationTitleView)
        topBarContentView.addSubview(menuButton)
    }
}

// MARK: - Title Regeneration

public extension ChatViewController {
    /// Regenerate the conversation title and emoji avatar.
    /// Shows a loading alert during generation and dismisses it upon completion.
    func regenerateTitle() {
        guard let session = currentSession else { return }

        let alert = UIAlertController(
            title: nil,
            message: String.localized("Generating…"),
            preferredStyle: .alert
        )
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        indicator.startAnimating()
        alert.view.addSubview(indicator)
        NSLayoutConstraint.activate([
            indicator.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor),
            indicator.leadingAnchor.constraint(equalTo: alert.view.leadingAnchor, constant: 20),
        ])

        present(alert, animated: true)

        Task { @MainActor in
            await session.regenerateTitle()
            alert.dismiss(animated: true) { [weak self] in
                self?.refreshNavigationTitle()
            }
        }
    }

    func clearConversation() {
        guard let session = currentSession else { return }

        draftInputObject = nil
        chatInputView.resetValues()
        chatInputView.storage.removeAll()
        chatInputView.bind(conversationID: conversationID)
        session.clear { [weak self] in
            Task { @MainActor in
                self?.messageListView.updateList()
                self?.refreshNavigationTitle()
            }
        }
    }
}

// MARK: - Tool Call Detail

extension ChatViewController {
    private func presentToolCallDetail(_ toolCall: ToolCallContentPart) {
        let detailVC = ToolCallDetailViewController(content: .init(
            toolName: toolCall.toolName,
            toolIcon: toolCall.toolIcon,
            parameters: toolCall.parameters,
            result: toolCall.result,
            state: toolCall.state,
            duration: toolCall.duration
        ))
        let nav = UINavigationController(rootViewController: detailVC)
        nav.modalPresentationStyle = .pageSheet
        if let sheet = nav.sheetPresentationController {
            sheet.detents = [.medium(), .large()]
            sheet.prefersGrabberVisible = true
        }
        present(nav, animated: true)
    }
}

// MARK: - Chat Input

extension ChatViewController: ChatInputDelegate {
    public func chatInputDidSubmit(_: ChatInputView, object: ChatInputContent, completion: @escaping @Sendable (Bool) -> Void) {
        guard let session = messageListView.session else {
            completion(false)
            return
        }
        guard let model = session.models.chat else {
            completion(false)
            return
        }
        let userInput = makeUserInput(from: object, workspacePath: sessionConfiguration.workspacePath)
        draftInputObject = nil
        session.runInference(model: model, messageListView: messageListView, input: userInput) {
            completion(true)
        }
    }

    public func chatInputDidUpdateObject(_: ChatInputView, object: ChatInputContent) {
        draftInputObject = object
    }

    public func chatInputDidRequestObjectForRestore(_: ChatInputView) -> ChatInputContent? {
        draftInputObject
    }

    public func chatInputDidReportError(_: ChatInputView, error: String) {
        let alert = UIAlertController(title: String.localized("Error"), message: error, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: String.localized("OK"), style: .default))
        present(alert, animated: true)
    }

    public func chatInputDidToggleVoiceSession(_: ChatInputView) {
        onVoiceSessionToggle?()
    }

    public func chatInputDidStopVoiceSession(_: ChatInputView) {
        onVoiceSessionStop?()
    }

    public func chatInputDidTapConversationList(_: ChatInputView) {
        onConversationListTap?()
    }

    public func chatInputDidTapPrompts(_: ChatInputView) {
        onPromptsTap?()
    }

    public func chatInputDidRequestDictation(_: ChatInputView) {
        onDictationRequested?()
    }

    public func chatInputDidCancelDictation(_: ChatInputView) {
        onDictationCancelled?()
    }

    public func chatInputDidBeginPushToTalk(_: ChatInputView) {
        onPushToTalkBegan?()
    }

    public func chatInputDidEndPushToTalk(_: ChatInputView) {
        onPushToTalkEnded?()
    }

    /// Programmatically set text in the input and submit it.
    public func submitText(_ text: String) {
        chatInputView.refill(withText: text, attachments: [])
        chatInputView.submitValues()
    }
}
