//
//  AuthenticationOperation.swift
//  AltStore
//
//  Created by Riley Testut on 6/5/19.
//  Copyright © 2019 Riley Testut. All rights reserved.
//

import Foundation
import Roxas
import Network
import AltStoreCore
import AltSign

private extension UIColor
{
    static let altInvertedPrimary = UIColor(named: "SettingsHighlighted")!
}

typealias AuthenticationError = AuthenticationErrorCode.Error
enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable
{
    case noTeam
    case noCertificate
    case teamSelectorError
    
    case missingPrivateKey
    case missingCertificate
    
    var errorFailureReason: String {
        switch self {
        case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "")
        case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "")
        case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "")
        case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "")
        case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "")
        }
    }
}

@objc(AuthenticationOperation)
final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{
    let context: AuthenticatedOperationContext
    
    private weak var presentingViewController: UIViewController?
    
    private lazy var navigationController: UINavigationController = {
        let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController
        navigationController.isModalInPresentation = true
        return navigationController
    }()
    
    private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil)
    
    private var appleIDEmailAddress: String?
    private var appleIDPassword: String?
    private var shouldShowInstructions = false
    
    private let operationQueue = OperationQueue()
    
    private var submitCodeAction: UIAlertAction?
    
    init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
    {
        self.context = context
        self.presentingViewController = presentingViewController
        
        super.init()
        
        self.context.authenticationOperation = self
        self.operationQueue.name = "com.altstore.AuthenticationOperation"
        self.progress.totalUnitCount = 4
    }
    
    override func main()
    {
        super.main()
        
        if let error = self.context.error
        {
            self.finish(.failure(error))
            return
        }

        Task {
            // try to use cached session
            if
                let certificate = Keychain.shared.certificate,
                let session = Keychain.shared.session,
                let team = Keychain.shared.team
            {
                if session.anisetteData.date.timeIntervalSinceNow < -40.0 {
                    let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
                        let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
                        fetchAnisetteDataOperation.resultHandler = { (result) in
                            c.resume(with: result)
                        }
                        self.operationQueue.addOperation(fetchAnisetteDataOperation)
                    }
                    session.anisetteData = anisetteData
                }
                self.context.team = team
                self.context.session = session
                self.context.certificate = certificate
                self.finish(.success((team, certificate, session)))
                return
            }
            
            // new login
            do {
                let (account, session) = try await withUnsafeThrowingContinuation { c in
                    self.signIn() { (result) in
                        c.resume(with: result)
                    }
                }
                self.context.session = session
                self.progress.completedUnitCount += 1
                guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
                
                let team = try await withUnsafeThrowingContinuation { c in
                    self.fetchTeam(for: account, session: session) { (result) in
                        c.resume(with: result)
                    }
                }
                self.context.team = team
                self.progress.completedUnitCount += 1
                guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
                
                let certificate = try await withUnsafeThrowingContinuation { c in
                    self.fetchCertificate(for: team, session: session) { (result) in
                        c.resume(with: result)
                    }
                }
                self.context.certificate = certificate
                self.progress.completedUnitCount += 1
                guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
                
                let _ = try await withUnsafeThrowingContinuation { c in
                    self.registerCurrentDevice(for: team, session: session) { (result) in
                        c.resume(with: result)
                    }
                }
                self.progress.completedUnitCount += 1
                guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
                
                try await withUnsafeThrowingContinuation { c in
                    self.save(team) { (result) in
                        c.resume(with: result)
                    }
                }
                guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
                
                try await withUnsafeThrowingContinuation { c in
                    self.cacheAppIDs(team: team, session: session) { (result) in
                        c.resume(with: result)
                    }
                }
                Keychain.shared.team = team
                Keychain.shared.certificate = certificate
                Keychain.shared.session = session
                self.finish(.success((team, certificate, session)))

            } catch {
                self.finish(.failure(error))
            }
        }
    }
    
    func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result<Void, Error>) -> Void)
    {
        let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
        context.performAndWait {
            do
            {
                let account: Account
                let team: Team
                
                if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context)
                {
                    account = tempAccount
                }
                else
                {
                    account = Account(altTeam.account, context: context)
                }
                
                if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
                {
                    team = tempTeam
                }
                else
                {
                    team = Team(altTeam, account: account, context: context)
                }
                
                account.update(account: altTeam.account)
                
                if let providedEmailAddress = self.appleIDEmailAddress
                {
                    // Save the user's provided email address instead of the one associated with their account (which may be outdated).
                    account.appleID = providedEmailAddress
                }
                
                team.update(team: altTeam)
                                
                try context.save()
                
                completionHandler(.success(()))
            }
            catch
            {
                completionHandler(.failure(error))
            }
        }
    }
    
    override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
    {
        guard !self.isFinished else { return }
        
        switch result
        {
        case .failure(let error): Logger.sideload.error("Failed to authenticate account. \(error.localizedDescription, privacy: .public)")
        case .success((let team, _, _)): Logger.sideload.notice("Authenticated account for team \(team.identifier, privacy: .public).")
        }
        
        let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
        context.perform {
            do
            {
                let (altTeam, altCertificate, session) = try result.get()
                
                guard
                    let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
                    let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
                else { throw AuthenticationError(.noTeam) }
                // Account
                account.isActiveAccount = true
                
                let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest<Account>
                otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier)
                
                let otherAccounts = try context.fetch(otherAccountsFetchRequest)
                for account in otherAccounts
                {
                    account.isActiveAccount = false
                }
                
                // Team
                team.isActiveTeam = true
                
                let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest<Team>
                otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier)
                
                let otherTeams = try context.fetch(otherTeamsFetchRequest)
                for team in otherTeams
                {
                    team.isActiveTeam = false
                }

                let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)

                let isMinimumVersionMatching = ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
                let isSparseRestorePatched   = ProcessInfo().sparseRestorePatched
                let isAppLimitDisabled       = UserDefaults.standard.isAppLimitDisabled

                UserDefaults.standard.activeAppsLimit = nil
                // TODO: @mahee96: is the minimum ver match for ios 13.3.1 check required?
                //                 if so what is the app limit? As nil app limit specifies unlimited apps?!
                if team.type == .free//, isMinimumVersionMatching 
                {
                    if (!isAppLimitDisabled && isSparseRestorePatched) ||
                        (isAppLimitDisabled && !isSparseRestorePatched)
                    {
                         UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
                    }
                }
                
                // Save
                try context.save()
                
                // Update keychain
                Keychain.shared.appleIDEmailAddress = self.appleIDEmailAddress ?? altTeam.account.appleID // Prefer the user's provided email address over the one associated with their account (which may be outdated).
                Keychain.shared.appleIDPassword = self.appleIDPassword
                
                Keychain.shared.signingCertificate = altCertificate.p12Data()
                Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
                
                self.showInstructionsIfNecessary() { (didShowInstructions) in
                    
                    let signer = ALTSigner(team: altTeam, certificate: altCertificate)
                    // Refresh screen must go last since a successful refresh will cause the app to quit.
                    self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
                        super.finish(result)
                        
                        DispatchQueue.main.async {
                            self.navigationController.dismiss(animated: true, completion: nil)
                        }
                    }
                }
            }
            catch
            {
                super.finish(result)
                
                DispatchQueue.main.async {
                    self.navigationController.dismiss(animated: true, completion: nil)
                }
            }
        }
    }
}

private extension AuthenticationOperation
{
    func present(_ viewController: UIViewController) -> Bool
    {
        guard let presentingViewController = self.presentingViewController else { return false }
        
        self.navigationController.view.tintColor = .altInvertedPrimary
        
        if self.navigationController.viewControllers.isEmpty
        {
            guard presentingViewController.presentedViewController == nil else { return false }
            
            self.navigationController.setViewControllers([viewController], animated: false)            
            presentingViewController.present(self.navigationController, animated: true, completion: nil)
        }
        else
        {
            viewController.navigationItem.leftBarButtonItem = nil
            self.navigationController.pushViewController(viewController, animated: true)
        }
        
        return true
    }
}

private extension AuthenticationOperation
{
    func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
    {
        func authenticate()
        {
            DispatchQueue.main.async {
                let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController
                authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in
                    self.authenticate(appleID: appleID, password: password) { (result) in
                        completionHandler(result)
                    }
                }
                authenticationViewController.completionHandler = { (result) in
                    if let (account, session, password) = result
                    {
                        // We presented the Auth UI and the user signed in.
                        // In this case, we'll assume we should show the instructions again.
                        self.shouldShowInstructions = true
                        
                        self.appleIDPassword = password
                        completionHandler(.success((account, session)))
                    }
                    else
                    {
                        completionHandler(.failure(OperationError.cancelled))
                    }
                }
                
                if !self.present(authenticationViewController)
                {
                    completionHandler(.failure(OperationError.notAuthenticated))
                }
            }
        }
        
        if let adsid = Keychain.shared.appleIDAdsid, let xcodeToken = Keychain.shared.appleIDXcodeToken {
            Logger.sideload.notice("Authenticating Apple ID with tokens...")
            let semaphore = DispatchSemaphore(value: 0)
            var shouldContinue = true
            Task {
                defer {
                    semaphore.signal()
                }
                do {
                    let (account, session) = try await self.authenticateWithToken(adsid: adsid, xcodeToken: xcodeToken)
                    completionHandler(.success((account, session)))
                    shouldContinue = false
                } catch {
                    Logger.sideload.notice("Authentication failed with token. Fall back to email and password login: \(error)")
                }
            }
            
            semaphore.wait()
            if !shouldContinue {
                return
            }
        }
        
        if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword
        {
            Logger.sideload.notice("Authenticating Apple ID...")
            
            self.authenticate(appleID: appleID, password: password) { (result) in
                switch result
                {
                case .success((let account, let session)):
                    self.appleIDPassword = password
                    completionHandler(.success((account, session)))
                    
                case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired):
                    authenticate()
                    
                case .failure(let error):
                    completionHandler(.failure(error))
                }
            }
        }
        else
        {
            authenticate()
        }
    }
    
    func authenticateWithToken(adsid: String, xcodeToken: String) async throws -> (ALTAccount, ALTAppleAPISession) {
        let anisetteData = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAnisetteData, any Error>) in
            let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
            fetchAnisetteDataOperation.resultHandler = { (result) in
                c.resume(with: result)
            }
            self.operationQueue.addOperation(fetchAnisetteDataOperation)
        }
        
        let session = ALTAppleAPISession(dsid: adsid, authToken: xcodeToken, anisetteData: anisetteData)
        let account = try await withUnsafeThrowingContinuation { (c: UnsafeContinuation<ALTAccount, any Error>) in
            ALTAppleAPI.shared.fetchAccount2(session: session) { result in
                c.resume(with: result)
            }
        }
        
        return (account, session)
    }
    
    func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
    {
        self.appleIDEmailAddress = appleID
        
        let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
        fetchAnisetteDataOperation.resultHandler = { (result) in
            switch result
            {
            case .failure(let error): completionHandler(.failure(error))
            case .success(let anisetteData):
                let verificationHandler: ((@escaping (String?) -> Void) -> Void)?
                
                if let presentingViewController = self.presentingViewController
                {
                    verificationHandler = { (completionHandler) in
                        DispatchQueue.main.async {
                            let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert)
                            alertController.addTextField { (textField) in
                                textField.autocorrectionType = .no
                                textField.autocapitalizationType = .none
                                textField.keyboardType = .numberPad
                                
                                NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField)
                            }
                            
                            let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in
                                let textField = alertController.textFields?.first
                                
                                let code = textField?.text ?? ""
                                completionHandler(code)
                            }
                            submitAction.isEnabled = false
                            alertController.addAction(submitAction)
                            self.submitCodeAction = submitAction
                            
                            alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in
                                completionHandler(nil)
                            })
                            
                            if self.navigationController.presentingViewController != nil
                            {
                                self.navigationController.present(alertController, animated: true, completion: nil)
                            }
                            else
                            {
                                presentingViewController.present(alertController, animated: true, completion: nil)
                            }
                        }
                    }
                }
                else
                {
                    // No view controller to present security code alert, so don't provide verificationHandler.
                    verificationHandler = nil
                }
                    
                ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData,
                                                verificationHandler: verificationHandler) { (account, session, error) in
                    if let account = account, let session = session
                    {
                        Keychain.shared.appleIDAdsid = session.dsid
                        Keychain.shared.appleIDXcodeToken = session.authToken
                        completionHandler(.success((account, session)))
                    }
                    else
                    {
                        completionHandler(.failure(error ?? OperationError.unknown()))
                    }
                }
            }
        }
        
        self.operationQueue.addOperation(fetchAnisetteDataOperation)
    }
    
    func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Swift.Error>) -> Void)
    {
        func selectTeam(from teams: [ALTTeam])
         {
             if teams.count <= 1 {
                 if let team = teams.first {
                     return completionHandler(.success(team))
                 } else {
                     return completionHandler(.failure(AuthenticationError(.noTeam)))
                 }
             } else {
                 DispatchQueue.main.async {
                     let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController

                     selectTeamViewController.teams = teams
                     selectTeamViewController.completionHandler = completionHandler

                     if !self.present(selectTeamViewController)
                     {
                         return completionHandler(.failure(AuthenticationError(.noTeam)))
                     }
                 }
             }
         }

        ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
            switch Result(teams, error)
            {
            case .failure(let error): completionHandler(.failure(error))
            case .success(let teams):
                DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
                    if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier })
                    {
                        completionHandler(.success(altTeam))
                    }
                    else
                    {
                        selectTeam(from: teams)
                    }
                }
            }
        }
    }
    
    func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Swift.Error>) -> Void)
    {
        func requestCertificate()
        {
            let machineName: String = "SideStore - \(team.account.firstName)'s \(UIDevice.current.name)"
            ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in
                do
                {
                    let certificate = try Result(certificate, error).get()
                    guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) }
                    ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
                        do
                        {
                            let certificates = try Result(certificates, error).get()
                            
                            guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else {
                                throw AuthenticationError(.missingCertificate)
                            }
                            
                            certificate.privateKey = privateKey
                            completionHandler(.success(certificate))
                        }
                        catch
                        {
                            completionHandler(.failure(error))
                        }
                    }
                }
                catch
                {
                    completionHandler(.failure(error))
                }
            }
        }
        
        func replaceCertificate(from certificates: [ALTCertificate])
        {
            let ourCertificates = certificates.filter { a in
                a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true
            }
            
            if ourCertificates.isEmpty {
                return requestCertificate()
            }
            
            // We don't have private keys for any of the certificates,
            // so we need to revoke one and create a new one.
            var certsText = ""
            for certificate in ourCertificates {
                if let name = certificate.machineName {
                    certsText.append("\(name)\n")
                }
            }
            
            DispatchQueue.main.async {
                let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert)
                
                let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in
                    requestCertificate()
                }
                let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in
                    for certificate in ourCertificates {
                        ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in
                            if let error = error, !success
                            {
                                completionHandler(.failure(error))
                            }
                        }
                    }
                    requestCertificate()
                }
                alertController.addAction(noAction)
                alertController.addAction(yesAction)
                
                if self.navigationController.presentingViewController != nil
                {
                    self.navigationController.present(alertController, animated: true, completion: nil)
                }
                else
                {
                    self.presentingViewController?.present(alertController, animated: true, completion: nil)
                }
            }
        }
        
        ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
            do
            {
                let certificates = try Result(certificates, error).get()
                
                if
                    let data = Keychain.shared.signingCertificate,
                    let localCertificate = ALTCertificate(p12Data: data, password: nil),
                    let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber })
                {
                    // We have a certificate stored in the keychain and it hasn't been revoked.
                    localCertificate.machineIdentifier = certificate.machineIdentifier
                    completionHandler(.success(localCertificate))
                }
                else if
                    let serialNumber = Keychain.shared.signingCertificateSerialNumber,
                    let privateKey = Keychain.shared.signingCertificatePrivateKey,
                    let certificate = certificates.first(where: { $0.serialNumber == serialNumber })
                {
                    // LEGACY
                    // We have the private key for one of the certificates, so add it to certificate and use it.
                    certificate.privateKey = privateKey
                    completionHandler(.success(certificate))
                }
                else if
                    let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String,
                    let certificate = certificates.first(where: { $0.serialNumber == serialNumber }),
                    let machineIdentifier = certificate.machineIdentifier,
                    FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path),
                    let data = try? Data(contentsOf: Bundle.main.certificateURL),
                    let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier)
                {
                    // We have an embedded certificate that hasn't been revoked.
                    localCertificate.machineIdentifier = machineIdentifier
                    completionHandler(.success(localCertificate))
                }
                else if certificates.isEmpty
                {
                    // No certificates, so request a new one.
                    requestCertificate()
                }
                else
                {
                    // We don't have private keys for any of the certificates,
                    // so we need to revoke one and create a new one.
                    replaceCertificate(from: certificates)
                }
            }
            catch
            {
                completionHandler(.failure(error))
            }
        }
    }
    
    func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
    {
        guard let udid = fetchUDID() else {
            return completionHandler(.failure(OperationError.unknownUDID))
        }
        
        ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in
            do
            {
                let devices = try Result(devices, error).get()
                
                if let device = devices.first(where: { $0.identifier == udid })
                {
                    completionHandler(.success(device))
                }
                else
                {
                    ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in
                        completionHandler(Result(device, error))
                    }
                }
            }
            catch
            {
                completionHandler(.failure(error))
            }
        }
    }
    
    func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
    {
        let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
        fetchAppIDsOperation.resultHandler = { (result) in
            do
            {
                let (_, context) = try result.get()
                try context.save()
                
                completionHandler(.success(()))
            }
            catch
            {
                completionHandler(.failure(error))
            }
        }
        
        self.operationQueue.addOperation(fetchAppIDsOperation)
    }
    
    func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void)
    {
        guard self.shouldShowInstructions else { return completionHandler(false) }
        
        DispatchQueue.main.async {
            let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController
            instructionsViewController.showsBottomButton = true
            instructionsViewController.completionHandler = {
                completionHandler(true)
            }
            
            if !self.present(instructionsViewController)
            {
                completionHandler(false)
            }
        }
    }
    
    func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void)
    {
        guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) }
        
        // If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
        guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
        
//        #if DEBUG && targetEnvironment(simulator)
//        completionHandler(false)
//        #else
        
        DispatchQueue.main.async {
            let context = AuthenticatedOperationContext(context: self.context)
            context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
            
            let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
            refreshViewController.context = context
            refreshViewController.completionHandler = { _ in
                completionHandler(true)
            }
            
            if !self.present(refreshViewController)
            {
                completionHandler(false)
            }
        }
//        #endif
    }
}

extension AuthenticationOperation
{
    @objc func textFieldTextDidChange(_ notification: Notification)
    {
        guard let textField = notification.object as? UITextField else { return }
        
        self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6
    }
}


extension ALTAppleAPI {
    func fetchAccount2(session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAccount, Error>) -> Void)
    {
        let url = URL(string: "viewDeveloper.action", relativeTo: self.baseURL)!
        
        self.sendRequest(with: url, additionalParameters: nil, session: session, team: nil) { (responseDictionary, requestError) in
            do
            {
                guard let responseDictionary = responseDictionary else { throw requestError ?? ALTAppleAPIError.unknown() }
                
                guard let account = try self.processResponse(responseDictionary, parseHandler: { () -> Any? in
                    guard let dictionary = responseDictionary["developer"] as? [String: Any] else { return nil }
                    let account = ALTAccount(responseDictionary: dictionary)
                    return account
                }, resultCodeHandler: nil) as? ALTAccount else {
                    throw ALTAppleAPIError.unknown()
                }

                completionHandler(.success(account))
            } catch {
                completionHandler(.failure(error))
            }
        }
    }
}
