//
//  InstanceSelectorTableViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 9/15/19.
//  Copyright © 2019 Shadowfacts. All rights reserved.
//

import UIKit
import Combine
import Pachyderm

protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
    func didSelectInstance(url: URL)
}

fileprivate let instanceCell = "instanceCell"

class InstanceSelectorTableViewController: UITableViewController {
    
    static var blocks: [NSRegularExpression] = {
        guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
            let array = NSArray(contentsOfFile: path) as? [String] else { return [] }
        return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
    }()
    
    weak var delegate: InstanceSelectorTableViewControllerDelegate?

    var dataSource: DataSource!
    var searchController: UISearchController!
    
    var recommendedInstances: [InstanceSelector.Instance] = []
    
    let urlCheckerSubject = PassthroughSubject<String?, Never>()
    var urlHandler: AnyCancellable?
    var currentQuery: String?
    
    private var activityIndicator: UIActivityIndicatorView!
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .portrait
        } else {
            return .all
        }
    }
    
    init() {
        super.init(style: .grouped)
        
        title = NSLocalizedString("Choose Your Instance", comment: "onboarding screen title")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // disable transparent background when scrolled to top because it gets weird with animating table items in and out
        let appearance = UINavigationBarAppearance()
        appearance.configureWithDefaultBackground()
        navigationItem.scrollEdgeAppearance = appearance

        tableView.backgroundColor = .appGroupedBackground
        tableView.keyboardDismissMode = .interactive
        tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 120
        createActivityIndicatorHeader()
        
        dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
            switch item {
            case let .selected(_, instance):
                let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
                cell.updateUI(instance: instance)
                return cell
            case let .recommended(instance):
                let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
                cell.updateUI(instance: instance)
                return cell
            }
        })
        
        searchController = UISearchController(searchResultsController: nil)
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.hidesNavigationBarDuringPresentation = false
        searchController.searchBar.searchTextField.autocapitalizationType = .none
        searchController.searchBar.searchTextField.keyboardType = .URL
        searchController.searchBar.showsCancelButton = false
        searchController.searchBar.placeholder = "Search or enter a URL"
        navigationItem.searchController = searchController
        navigationItem.hidesSearchBarWhenScrolling = false
        if #available(iOS 16.0, *) {
            navigationItem.preferredSearchBarPlacement = .stacked
        }
        definesPresentationContext = true
        
        urlHandler = urlCheckerSubject
            .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
            .map { [weak self] (s) -> String in
                if !s.isEmpty {
                    self?.activityIndicator.startAnimating()
                }
                return s
            }
            .debounce(for: .seconds(1), scheduler: RunLoop.main)
            .sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        loadRecommendedInstances()
    }
    
    private func parseURLComponents(input: String) -> URLComponents? {
        // we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
        var input = input
        var components = URLComponents()
        // extract protocol
        if input.contains("://") {
            let parts = input.components(separatedBy: "://")
            components.scheme = parts.first!
            input = parts.last!
        }
        if components.scheme != "https" && components.scheme != "http" {
            components.scheme = "https"
        }
        // drop path
        if input.contains("/") {
            let parts = input.components(separatedBy: "/")
            input = parts.first!
        }
        // parse port
        if input.contains(":") {
            let parts = input.components(separatedBy: ":")
            input = parts.first!
            components.port = Int(parts.last!)
        }
        components.host = input
        if Self.blocks.contains(where: { $0.numberOfMatches(in: input, range: NSRange(location: 0, length: input.utf16.count)) > 0 }) {
            return nil
        }
        return components
    }
    
    private func updateSpecificInstance(domain: String) {
        activityIndicator.startAnimating()
        
        guard let components = parseURLComponents(input: domain),
              let url = components.url else {
            var snapshot = dataSource.snapshot()
            if snapshot.indexOfSection(.selected) != nil {
                snapshot.deleteSections([.selected])
                dataSource.apply(snapshot)
            }
            activityIndicator.stopAnimating()
            return
        }

        let client = Client(baseURL: url, session: .appDefault)
        let request = Client.getInstance()
        client.run(request) { (response) in
            var snapshot = self.dataSource.snapshot()
            if snapshot.indexOfSection(.selected) != nil {
                snapshot.deleteSections([.selected])
            }

            if case let .success(instance, _) = response {
                if snapshot.indexOfSection(.recommendedInstances) != nil {
                    snapshot.insertSections([.selected], beforeSection: .recommendedInstances)
                } else {
                    snapshot.appendSections([.selected])
                }
                
                snapshot.appendItems([.selected(url, instance)], toSection: .selected)
                
                DispatchQueue.main.async {
                    self.dataSource.apply(snapshot) {
                        self.activityIndicator.stopAnimating()
                    }
                }
            } else {
                DispatchQueue.main.async {
                    self.activityIndicator.stopAnimating()
                }
            }
        }
    }
    
    private func loadRecommendedInstances() {
        InstanceSelector.getInstances(category: nil) { (response) in
            DispatchQueue.main.async {
                switch response {
                case let .failure(error):
                    self.showRecommendationsError(error)
                case let .success(instances):
                    self.recommendedInstances = instances
                    self.filterRecommendedResults()
                }
            }
        }
    }
    
    private func createActivityIndicatorHeader() {
        let header = UITableViewHeaderFooterView()
        header.translatesAutoresizingMaskIntoConstraints = false
        header.contentView.backgroundColor = .appGroupedBackground
        
        activityIndicator = UIActivityIndicatorView(style: .large)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        header.contentView.addSubview(activityIndicator)
        
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: header.contentView.centerXAnchor),
            activityIndicator.topAnchor.constraint(equalTo: header.contentView.topAnchor, constant: 4),
            activityIndicator.bottomAnchor.constraint(equalTo: header.contentView.bottomAnchor, constant: -4),
        ])
        
        let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
        let size = header.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
        header.frame = CGRect(origin: .zero, size: size)
        
        tableView.tableHeaderView = header
    }
    
    private func showRecommendationsError(_ error: Client.ErrorType) {
        let footer = UITableViewHeaderFooterView()
        footer.translatesAutoresizingMaskIntoConstraints = false
        
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textColor = .secondaryLabel
        label.textAlignment = .center
        label.font = .boldSystemFont(ofSize: 17)
        label.numberOfLines = 0
        label.text = "Could not fetch suggested instances: \(error.localizedDescription)"

        footer.contentView.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalToSystemSpacingAfter: footer.contentView.leadingAnchor, multiplier: 1),
            footer.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1),
            label.topAnchor.constraint(equalTo: footer.contentView.topAnchor, constant: 8),
            label.bottomAnchor.constraint(equalTo: footer.contentView.bottomAnchor, constant: 8),
        ])
        
        let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
        let size = footer.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
        footer.frame = CGRect(origin: .zero, size: size)
        
        tableView.tableFooterView = footer
    }
    
    private func filterRecommendedResults() {
        let filteredInstances: [InstanceSelector.Instance]
        if let currentQuery = currentQuery, !currentQuery.isEmpty {
            filteredInstances = recommendedInstances.filter {
                $0.domain.contains(currentQuery) || $0.description.lowercased().contains(currentQuery)
            }
        } else {
            filteredInstances = recommendedInstances
        }

        var snapshot = self.dataSource.snapshot()
        if snapshot.indexOfSection(.recommendedInstances) != nil {
            let toRemove = snapshot.itemIdentifiers(inSection: .recommendedInstances).filter {
                if case .recommended(_) = $0 {
                    return true
                } else {
                    return false
                }
            }
            snapshot.deleteItems(toRemove)
        } else {
            snapshot.appendSections([.recommendedInstances])
        }
        snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
        self.dataSource.apply(snapshot)
    }
    
    // MARK: - Table view delegate
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let delegate = delegate, let item = dataSource.itemIdentifier(for: indexPath) else {
            return
        }
        switch item {
        case let .selected(url, _):
            // We can't rely on the URI reported by the instance API endpoint, because improperly configured instances may
            // return a domain that they don't listen on. Instead, use the actual base URL that was used to make the /api/v1/instance
            // request, since we know for certain that succeeded, otherwise this item wouldn't exist.
            delegate.didSelectInstance(url: url)
        case let .recommended(instance):
            var components = URLComponents()
            components.scheme = "https"
            components.host = instance.domain
            components.path = "/"
            delegate.didSelectInstance(url: components.url!)
        }
    }
}

extension InstanceSelectorTableViewController {
    enum Section {
        case selected
        case recommendedInstances
    }
    enum Item: Equatable, Hashable {
        case selected(URL, Instance)
        case recommended(InstanceSelector.Instance)
        
        static func ==(lhs: Item, rhs: Item) -> Bool {
            switch (lhs, rhs) {
            case let (.selected(urlA, instanceA), .selected(urlB, instanceB)):
                return urlA == urlB && instanceA.uri == instanceB.uri
            case let (.recommended(a), .recommended(b)):
                return a.domain == b.domain
            default:
                return false
            }
        }
        
        func hash(into hasher: inout Hasher) {
            switch self {
            case let .selected(url, instance):
                hasher.combine(0)
                hasher.combine(url)
                hasher.combine(instance.uri)
            case let .recommended(instance):
                hasher.combine(1)
                hasher.combine(instance.domain)
            }
        }
    }
    class DataSource: UITableViewDiffableDataSource<Section, Item> {
        
    }
}

extension InstanceSelectorTableViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        currentQuery = searchController.searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        filterRecommendedResults()
        urlCheckerSubject.send(currentQuery)
    }
}
