//
//  WealthsimpleDownloader.swift
//
//
//  Created by Steffen Kötte on 2020-07-12.
//

import Foundation

/// Protocol to save API tokens
///
/// Can for example be implemented using Keychain on Apple devices
public protocol CredentialStorage {

    /// Save a value to the store
    /// - Parameters:
    ///   - value: value
    ///   - key: key to retrieve in later
    func save(_ value: String, for key: String)

    /// Retrieve a value
    /// - Parameter key: key under which the value was stored
    ///
    /// - Returns: The saved value or nil if no value was found
    func read(_ key: String) -> String?

}

/// Main entry point for the library
public final class WealthsimpleDownloader {

    /// Callback which is called in case the user needs to authenticate. Needs to return username, password, and one time password
    public typealias AuthenticationCallback = (@escaping (String, String, String) -> Void) -> Void

    private let authenticationCallback: AuthenticationCallback
    private let credentialStorage: CredentialStorage
    private var token: Token?

    /// Creates the Downloader instance
    ///
    /// After creating, first call the authenticate method.
    ///
    /// - Parameters:
    ///   - authenticationCallback: Callback which is called in case the user needs to authenticate.
    ///     Needs to return username, password, and one time password. Might be called during any call.
    ///   - credentialStorage: A CredentialStore to save API tokens to. Implementation can be empty,
    ///     in this case the authenticationCallback will be called every time and not only when the refresh token expired
    public init(authenticationCallback: @escaping AuthenticationCallback, credentialStorage: CredentialStorage) {
        self.authenticationCallback = authenticationCallback
        self.credentialStorage = credentialStorage
    }

    /// Authneticates against the API. Call before calling any other method.
    /// - Parameter completion: Gets an error in case something went wrong, otherwise nil
    public func authenticate(completion: @escaping (Error?) -> Void) {
        if let token {
            token.refreshIfNeeded {
                switch $0 {
                case .failure:
                    self.getNewToken(completion: completion)
                case let .success(newToken):
                    self.token = newToken
                    completion(nil)
                }
            }
            return
        } else {
            Token.getToken(from: credentialStorage) {
                if let token = $0 {
                    self.token = token
                    completion(nil)
                    return
                } else {
                    self.token = nil
                    self.getNewToken(completion: completion)
                    return
                }
            }
        }
    }

    /// Get all Accounts the user has access to
    /// - Parameter completion: Result with an array of `Account`s or an `Account.AccountError`
    public func getAccounts(completion: @escaping (Result<[Account], AccountError>) -> Void) {
        guard let token else {
            completion(.failure(.tokenError(.noToken)))
            return
        }
        WealthsimpleAccount.getAccounts(token: token) {
            if case let .failure(error) = $0 {
                if case .tokenError = error {
                    self.token = nil
                }
            }
            completion($0)
        }
    }

    /// Get all `Position`s from one `Account`
    /// - Parameters:
    ///   - account: Account to retreive positions for
    ///   - date: Date of which the positions should be downloaded. If not date is provided, not date is sent to the API. The API falls back to the current date.
    ///   - completion: Result with an array of `Position`s or an `Position.PositionError`
    public func getPositions(in account: Account, date: Date?, completion: @escaping (Result<[Position], PositionError>) -> Void) {
        guard let token else {
            completion(.failure(.tokenError(.noToken)))
            return
        }
        WealthsimplePosition.getPositions(token: token, account: account, date: date) {
            if case let .failure(error) = $0 {
                if case .tokenError = error {
                    self.token = nil
                }
            }
            completion($0)
        }
    }

    /// Get all `Transactions`s from one `Account`
    /// - Parameters:
    ///   - account: Account to retreive transactions from
    ///   - startDate: Date from which the transactions are downloaded. If not date is provided, not date is sent to the API. The API falls back to 30 days ago from today.
    ///   - completion: Result with an array of `Transactions`s or an `Transactions.TransactionsError`
    public func getTransactions(in account: Account, startDate: Date?, completion: @escaping (Result<[Transaction], TransactionError>) -> Void) {
        guard let token else {
            completion(.failure(.tokenError(.noToken)))
            return
        }
        WealthsimpleTransaction.getTransactions(token: token, account: account, startDate: startDate) {
            if case let .failure(error) = $0 {
                if case .tokenError = error {
                    self.token = nil
                }
            }
            completion($0)
        }
    }

    private func getNewToken(completion: @escaping (Error?) -> Void) {
        authenticationCallback { username, password, otp in
            Token.getToken(username: username, password: password, otp: otp, credentialStorage: self.credentialStorage) {
                switch $0 {
                case let .failure(error):
                    completion(error)
                case let .success(token):
                    self.token = token
                    completion(nil)
                }
            }
        }
    }

}