//
//  OnboardingViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 8/18/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import UIKit
import AuthenticationServices
import Pachyderm
import OSLog
import UserAccounts

protocol OnboardingViewControllerDelegate {
    @MainActor
    func didFinishOnboarding(account: UserAccountInfo)
}

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "OnboardingViewController")

class OnboardingViewController: UINavigationController {

    var onboardingDelegate: OnboardingViewControllerDelegate?
    
    var instanceSelector = InstanceSelectorTableViewController()
    
    var authenticationSession: ASWebAuthenticationSession?
    
    private var clientInfo: (url: URL, id: String, secret: String)?
    
    init() {
        super.init(rootViewController: instanceSelector)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        instanceSelector.delegate = self
    }
    
    @MainActor
    private func login(to instanceURL: URL) async {
        let dimmingView = UIView()
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        dimmingView.backgroundColor = .black.withAlphaComponent(0.1)
        
        let blur = UIBlurEffect(style: .prominent)
        let blurView = UIVisualEffectView(effect: blur)
        blurView.translatesAutoresizingMaskIntoConstraints = false
        blurView.layer.cornerRadius = 15
        blurView.layer.masksToBounds = true
        
        let spinner = UIActivityIndicatorView(style: .large)
        spinner.translatesAutoresizingMaskIntoConstraints = false
        spinner.startAnimating()
        
        let statusLabel = UILabel()
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        statusLabel.font = .preferredFont(forTextStyle: .headline)
        statusLabel.numberOfLines = 0
        statusLabel.textAlignment = .center
        
        blurView.contentView.addSubview(spinner)
        blurView.contentView.addSubview(statusLabel)
        dimmingView.addSubview(blurView)
        view.addSubview(dimmingView)
        NSLayoutConstraint.activate([
            dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
            dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            blurView.widthAnchor.constraint(equalToConstant: 150),
            blurView.heightAnchor.constraint(equalToConstant: 150),
            blurView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
            blurView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
            
            spinner.centerXAnchor.constraint(equalTo: blurView.contentView.centerXAnchor),
            spinner.bottomAnchor.constraint(equalTo: blurView.contentView.centerYAnchor, constant: -2),
            statusLabel.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
            statusLabel.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
            statusLabel.topAnchor.constraint(equalTo: blurView.contentView.centerYAnchor, constant: 2),
        ])
        
        dimmingView.layer.opacity = 0
        blurView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
            dimmingView.layer.opacity = 1
            blurView.transform = .identity
        }
        
        do {
            try await tryLogin(to: instanceURL) {
                statusLabel.text = $0
            }
        } catch Error.cancelled {
            // no-op, don't show an error message
        } catch {
            let message: String
            if let error = error as? Error {
                message = error.localizedDescription
            } else {
                message = error.localizedDescription
            }
            let alert = UIAlertController(title: "Error Logging In", message: message, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(alert, animated: true)
        }
        
        dimmingView.removeFromSuperview()
    }
    
    private func tryLogin(to instanceURL: URL, updateStatus: (String) -> Void) async throws {
        let mastodonController = MastodonController(instanceURL: instanceURL, transient: true)
        let clientID: String
        let clientSecret: String
        if let clientInfo, clientInfo.url == instanceURL {
            clientID = clientInfo.id
            clientSecret = clientInfo.secret
        } else {
            updateStatus("Registering App")
            do {
                (clientID, clientSecret) = try await mastodonController.registerApp()
                self.clientInfo = (instanceURL, clientID, clientSecret)
                updateStatus("Reticulating Splines")
                try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC)
            } catch {
                throw Error.registeringApp(error)
            }
        }
        updateStatus("Logging in")
        let authCode = try await getAuthorizationCode(instanceURL: instanceURL, clientID: clientID)
        updateStatus("Authorizing")
        let accessToken: String
        do {
            accessToken = try await retrying("Getting access token") {
                try await mastodonController.authorize(authorizationCode: authCode)
            }
        } catch {
            throw Error.gettingAccessToken(error)
        }
        
        // construct a temporary UserAccountInfo instance for the MastodonController to use to fetch its own account
        let tempAccountInfo = UserAccountInfo(tempInstanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, accessToken: accessToken)
        mastodonController.accountInfo = tempAccountInfo
        
        updateStatus("Checking Credentials")
        let ownAccount: AccountMO
        do {
            ownAccount = try await retrying("Getting own account") {
                try await mastodonController.getOwnAccount()
            }
        } catch {
            throw Error.gettingOwnAccount(error)
        }
        
        let accountInfo = UserAccountsManager.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: ownAccount.username, accessToken: accessToken)
        mastodonController.accountInfo = accountInfo
        
        self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
    }
    
    private func retrying<T>(_ label: StaticString, action: () async throws -> T) async throws -> T {
        for attempt in 0..<4 {
            do {
                return try await action()
            } catch {
                let seconds = (pow(2, attempt) as NSDecimalNumber).uint64Value
                logger.error("\(label, privacy: .public) failed, waiting \(seconds, privacy: .public)s before retrying. Reason: \(String(describing: error))")
                try! await Task.sleep(nanoseconds: seconds * NSEC_PER_SEC)
            }
        }
        return try await action()
    }
    
    @MainActor
    private func getAuthorizationCode(instanceURL: URL, clientID: String) async throws -> String {
        var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
        components.path = "/oauth/authorize"
        components.queryItems = [
            URLQueryItem(name: "client_id", value: clientID),
            URLQueryItem(name: "response_type", value: "code"),
            URLQueryItem(name: "scope", value: "read write follow"),
            URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
        ]
        let authorizeURL = components.url!
        
        return try await withCheckedThrowingContinuation({ continuation in
            self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
                if let error = error {
                    if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
                        continuation.resume(throwing: Error.cancelled)
                    } else {
                        continuation.resume(throwing: Error.authenticationSessionError(error))
                    }
                } else if let url = url,
                          let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
                          let item = components.queryItems?.first(where: { $0.name == "code" }),
                          let code = item.value {
                    continuation.resume(returning: code)
                } else {
                    continuation.resume(throwing: Error.noAuthorizationCode)
                }
            })
            // Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
            self.authenticationSession!.prefersEphemeralWebBrowserSession = true
            self.authenticationSession!.presentationContextProvider = self
            self.authenticationSession!.start()
        })
    }

}

extension OnboardingViewController {
    enum Error: Swift.Error {
        case cancelled
        case registeringApp(Swift.Error)
        case authenticationSessionError(Swift.Error)
        case noAuthorizationCode
        case gettingAccessToken(Swift.Error)
        case gettingOwnAccount(Swift.Error)
        
        var localizedDescription: String {
            switch self {
            case .cancelled:
                return "Login Cancelled"
            case .registeringApp(let error):
                return "Couldn't register app: \(error)"
            case .authenticationSessionError(let error):
                return error.localizedDescription
            case .noAuthorizationCode:
                return "No authorization code"
            case .gettingAccessToken(let error):
                return "Couldn't get access token: \(error)"
            case .gettingOwnAccount(let error):
                return "Couldn't fetch account: \(error)"
            }
        }
    }
}

extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
    func didSelectInstance(url instanceURL: URL) {
        Task {
            await self.login(to: instanceURL)
            instanceSelector.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
        }
    }
}

extension OnboardingViewController: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return view.window!
    }
}
