//
//  ConversationViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 1/17/23.
//  Copyright © 2023 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import WebURL
import WebURLFoundationExtras

private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
    let path = url.path
    let range = NSRange(location: 0, length: path.utf16.count)
    return mastodonRemoteStatusRegex.numberOfMatches(in: path, range: range) == 1
}

class ConversationViewController: UIViewController {
    
    weak var mastodonController: MastodonController!
    
    private(set) var mode: Mode
    let mainStatusState: CollapseState
    var statusIDToScrollToOnLoad: String?
    var showStatusesAutomatically = false {
        didSet {
            if case .displaying(let vc) = state {
                vc.showStatusesAutomatically = showStatusesAutomatically
            }
        }
    }
    
    private var collapseBarButtonItem: UIBarButtonItem!
    
    private var state: State = .unloaded {
        didSet {
            switch oldValue {
            case .loading(let indicator):
                indicator.removeFromSuperview()
            case .displaying(let vc):
                vc.removeViewAndController()
            default:
                break
            }
            
            switch state {
            case .unloaded:
                break
            case .loading(let indicator):
                indicator.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(indicator)
                NSLayoutConstraint.activate([
                    indicator.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
                    indicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
                ])
            case .displaying(let vc):
                embedChild(vc)
            case .notFound:
                showMainStatusNotFound()
            case .unableToResolve(let error):
                showUnableToResolve(error)
            }
            
            updateVisibilityBarButtonItem()
        }
    }
    
    init(for mainStatusID: String, state mainStatusState: CollapseState, mastodonController: MastodonController) {
        self.mode = .localID(mainStatusID)
        self.mainStatusState = mainStatusState
        self.mastodonController = mastodonController
        
        super.init(nibName: nil, bundle: nil)
    }
    
    init(resolving url: URL, mastodonController: MastodonController) {
        self.mode = .resolve(url)
        self.mainStatusState = .unknown
        self.mastodonController = mastodonController
        
        super.init(nibName: nil, bundle: nil)
    }
    
    init(preloadedTree: ConversationTree, state mainStatusState: CollapseState, mastodonController: MastodonController) {
        self.mode = .preloaded(preloadedTree)
        self.mainStatusState = mainStatusState
        self.mastodonController = mastodonController

        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = NSLocalizedString("Conversation", comment: "conversation screen title")

        view.backgroundColor = .appSecondaryBackground
        
        collapseBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "eye.fill")!, style: .plain, target: self, action: #selector(toggleCollapseButtonPressed))
        updateVisibilityBarButtonItem()
        navigationItem.rightBarButtonItem = collapseBarButtonItem
        // disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
        // (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
        let appearance = UINavigationBarAppearance()
        appearance.configureWithDefaultBackground()
        navigationItem.scrollEdgeAppearance = appearance
        
        NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
    }
    
    private func updateVisibilityBarButtonItem() {
        switch state {
        case .loading(_), .displaying(_):
            collapseBarButtonItem.isEnabled = true
        default:
            collapseBarButtonItem.isEnabled = false
        }
        
        collapseBarButtonItem.isSelected = showStatusesAutomatically
        collapseBarButtonItem.accessibilityLabel = showStatusesAutomatically ? "Collapse All" : "Expand All"
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        if case .unloaded = state {
            if case .preloaded(let tree) = mode {
                // when everything is preloaded, we're on the fast path and want to avoid any async work
                // just kicking off a MainActor task causes a delay before the content appears, even if the task doesn't suspend
                mainStatusLoaded(tree.mainStatus.status)
            } else {
                Task { @MainActor in
                    await loadMainStatus()
                }
            }
        }
    }
    
    @objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
        guard let userInfo = notification.userInfo,
              let statusIDs = userInfo["statusIDs"] as? [String],
              case .localID(let mainStatusID) = mode else {
            return
        }
        if statusIDs.contains(mainStatusID) {
            state = .notFound
        } else if case .displaying(_) = state {
            let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID)!
            Task {
                await loadContext(for: mainStatus)
            }
        }
    }
    
    // MARK: Loading
    
    @MainActor
    private func loadMainStatus() async {
        let mainStatusID: String
        switch mode {
        case .localID(let id):
            mainStatusID = id
        case .resolve(let url):
            if let id = await resolveStatus(url: url) {
                mainStatusID = id
            } else {
                return
            }
        case .preloaded(_):
            fatalError("unreachable")
        }
        
        @MainActor
        func doLoadMainStatus() async -> StatusMO? {
            switch await FetchStatusService(statusID: mainStatusID, mastodonController: mastodonController).run() {
            case .loaded(let status):
                return mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
            case .notFound:
                state = .notFound
                return nil
            case .error(let error):
                self.showMainStatusError(error)
                return nil
            }
        }
        
        if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
            // if we have a cached copy, display it immediately but still try to refresh it
            Task {
                await doLoadMainStatus()
            }
            mainStatusLoaded(cached)
        } else {
            // otherwise, show a loading indicator while loading the main status
            let indicator = UIActivityIndicatorView(style: .medium)
            indicator.startAnimating()
            state = .loading(indicator)
            
            if let status = await doLoadMainStatus() {
                mainStatusLoaded(status)
            }
        }
    }
    
    @MainActor
    private func resolveStatus(url: URL) async -> String? {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.startAnimating()
        state = .loading(indicator)
        
        let effectiveURL: String
        class RedirectBlocker: NSObject, URLSessionTaskDelegate {
            func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
                completionHandler(nil)
            }
        }
        if isLikelyMastodonRemoteStatus(url: url),
           let (_, response) = try? await URLSession.shared.data(from: url, delegate: RedirectBlocker()),
           let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
            effectiveURL = location
        } else {
            effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
        }
        
        let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
        do {
            let (results, _) = try await mastodonController.run(request)
            guard let status = results.statuses.first(where: { $0.url?.serialized() == effectiveURL }) else {
                throw UnableToResolveError()
            }
            _ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
            mode = .localID(status.id)
            return status.id
        } catch {
            state = .unableToResolve(error)
            return nil
        }
    }
    
    private func mainStatusLoaded(_ mainStatus: StatusMO) {
        if let accountID = mastodonController.accountInfo?.id {
            userActivity = UserActivityManager.showConversationActivity(mainStatusID: mainStatus.id, accountID: accountID)
        }
        
        let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, conversationViewController: self)
        vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
        vc.showStatusesAutomatically = showStatusesAutomatically
        vc.addMainStatus(mainStatus)
        state = .displaying(vc)
        
        if case .preloaded(let tree) = mode {
            vc.addTree(tree, mainStatus: mainStatus)
        } else {
            Task { @MainActor in
                await loadTree(for: mainStatus)
            }
        }
    }
    
    @MainActor
    private func loadTree(for mainStatus: StatusMO) async {
        guard case .displaying(_) = state,
              let context = await loadContext(for: mainStatus) else {
            return
        }
        
        await mastodonController.persistentContainer.addAll(statuses: context.ancestors + context.descendants)
        
        let ancestorIDs = context.ancestors.map(\.id)
        let ancestorsReq = StatusMO.fetchRequest()
        ancestorsReq.predicate = NSPredicate(format: "id in %@", ancestorIDs)
        let ancestors = try? mastodonController.persistentContainer.viewContext.fetch(ancestorsReq)
        
        let descendantIDs = context.descendants.map(\.id)
        let descendantsReq = StatusMO.fetchRequest()
        descendantsReq.predicate = NSPredicate(format: "id IN %@", descendantIDs)
        let descendants = try? mastodonController.persistentContainer.viewContext.fetch(descendantsReq)
        
        let tree = ConversationTree.build(for: mainStatus, ancestors: ancestors ?? [], descendants: descendants ?? [])
        
        guard case .displaying(let vc) = state else {
            return
        }
        vc.addTree(tree, mainStatus: mainStatus)
    }
    
    private func loadContext(for mainStatus: StatusMO) async -> ConversationContext? {
        let request = Status.getContext(mainStatus.id)
        do {
            let (context, _) = try await mastodonController.run(request)
            return context
        } catch {
            guard case .displaying(_) = state else {
                return nil
            }
            let error = error as! Client.Error
            
            let config = ToastConfiguration(from: error, with: "Error Loading Context", in: self) { [weak self] toast in
                toast.dismissToast(animated: true)
                await self?.loadTree(for: mainStatus)
            }
            self.showToast(configuration: config, animated: true)
            
            return nil
        }
    }
    
    func refreshContext() async {
        guard case .localID(let id) = mode,
              let status = mastodonController.persistentContainer.status(for: id),
              case .displaying(_) = state else {
            return
        }
        await loadTree(for: status)
    }
    
    private func showMainStatusNotFound() {
        let notFoundView = StatusNotFoundView(frame: .zero)
        notFoundView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(notFoundView)
        NSLayoutConstraint.activate([
            notFoundView.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
            view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: notFoundView.trailingAnchor, multiplier: 1),
            notFoundView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
        ])
    }
    
    private func showMainStatusError(_ error: Client.Error) {
        let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] toast in
            toast.dismissToast(animated: true)
            await self?.loadMainStatus()
        }
        self.showToast(configuration: config, animated: true)
    }
    
    private func showUnableToResolve(_ error: Error) {
        let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
        image.tintColor = .secondaryLabel
        image.contentMode = .scaleAspectFit
        
        let title = UILabel()
        title.textColor = .secondaryLabel
        title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
        title.adjustsFontForContentSizeCategory = true
        title.text = "Couldn't Load Post"
        
        let subtitle = UILabel()
        subtitle.textColor = .secondaryLabel
        subtitle.font = .preferredFont(forTextStyle: .body)
        subtitle.adjustsFontForContentSizeCategory = true
        subtitle.numberOfLines = 0
        subtitle.textAlignment = .center
        if let error = error as? UnableToResolveError {
            subtitle.text = error.localizedDescription
        } else if let error = error as? Client.Error {
            subtitle.text = error.localizedDescription
        } else {
            subtitle.text = error.localizedDescription
        }
        
        var config = UIButton.Configuration.plain()
        config.title = "Open in Safari"
        config.image = UIImage(systemName: "safari")
        config.imagePadding = 4
        let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [unowned self] _ in
            guard case .resolve(let url) = self.mode else {
                return
            }
            self.selected(url: url, allowResolveStatuses: false)
        }))
        
        let stack = UIStackView(arrangedSubviews: [
            image,
            title,
            subtitle,
            button,
        ])
        stack.axis = .vertical
        stack.alignment = .center
        stack.spacing = 8
        stack.isAccessibilityElement = true
        stack.accessibilityLabel = "\(title.text!). \(subtitle.text!)"
        
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)
        NSLayoutConstraint.activate([
            image.widthAnchor.constraint(equalToConstant: 64),
            image.heightAnchor.constraint(equalToConstant: 64),
            
            stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
            view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
            stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
        ])
    }
    
    // MARK: Interaction
    
    @objc func toggleCollapseButtonPressed() {
        guard case .displaying(let vc) = state else {
            return
        }
        showStatusesAutomatically = !showStatusesAutomatically
        vc.updateVisibleCellCollapseState()
        updateVisibilityBarButtonItem()
    }

}

extension ConversationViewController {
    enum Mode {
        case localID(String)
        case resolve(URL)
        case preloaded(ConversationTree)
    }
}

extension ConversationViewController {
    struct UnableToResolveError: Error {
        var localizedDescription: String {
            "Unable to resolve status from URL"
        }
    }
}

extension ConversationViewController {
    enum State {
        case unloaded
        case loading(UIActivityIndicatorView)
        case displaying(ConversationCollectionViewController)
        case notFound
        case unableToResolve(Error)
    }
}

extension ConversationViewController: TuskerNavigationDelegate {
    var apiController: MastodonController! { mastodonController }
}

extension ConversationViewController: StateRestorableViewController {
    func stateRestorationActivity() -> NSUserActivity? {
        if let accountID = mastodonController.accountInfo?.id,
           case .localID(let id) = mode {
            return UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountID)
        } else {
            return nil
        }
    }
}

extension ConversationViewController: ToastableViewController {
    var toastScrollView: UIScrollView? {
        if case .displaying(let vc) = state {
            return vc.toastScrollView
        } else {
            return nil
        }
    }
}

extension ConversationViewController: StatusBarTappableViewController {
    func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
        if case .displaying(let vc) = state {
            return vc.handleStatusBarTapped(xPosition: xPosition)
        } else {
            return .continue
        }
    }
}
