// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the ""Software""), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// --------------------------------------------------------------------------

import Foundation

internal class ThreadSafeRefreshableAccessTokenCache: Cancellable {
    private var currentToken: CommunicationAccessToken {
        didSet {
            maybeScheduleRefresh()
        }
    }

    private let scheduleProactivelyRefreshing: Bool
    private var proactiveRefreshTimer: Timer?

    private let proactiveRefreshingInterval = TimeInterval(600)
    private let onDemandRefreshingInterval = TimeInterval(120)
    private let refreshAfterTTLDivider = 2.0

    private let tokenRefresher: TokenRefreshAction
    let anyThreadRefreshing = DispatchSemaphore(value: 1)
    private typealias TimerAction = () -> Void
    internal typealias AccessTokenRefreshAction = (@escaping CommunicationTokenCompletionHandler) -> Void
    internal typealias TokenRefreshAction = (@escaping TokenRefreshHandler) -> Void

    public convenience init(refreshProactively: Bool, tokenRefresher: @escaping TokenRefreshAction) {
        self.init(
            refreshProactively: refreshProactively,
            initialValue: CommunicationAccessToken(token: "", expiresOn: Date()),
            tokenRefresher: tokenRefresher
        )
    }

    public init(
        refreshProactively: Bool,
        initialValue: CommunicationAccessToken,
        tokenRefresher: @escaping TokenRefreshAction
    ) {
        self.scheduleProactivelyRefreshing = refreshProactively
        self.currentToken = initialValue
        self.tokenRefresher = tokenRefresher

        // didSet is not called from the initialization context
        maybeScheduleRefresh()
    }

    private func refreshAccessToken(completionHandler: @escaping CommunicationTokenCompletionHandler) {
        tokenRefresher { newToken, error in
            if let error = error {
                completionHandler(nil, error)
                return
            }

            do {
                let newAccessToken = try JwtTokenParser.createAccessToken(newToken!)
                guard !self.isTokenExpired(accessToken: newAccessToken) else {
                    throw NSError(
                        domain: "AzureCommunicationCommon.ThreadSafeRefreshableAccessTokenCache.refreshAccessToken",
                        code: 0,
                        userInfo: ["message": "The token returned from the tokenRefresher is expired."]
                    )
                }
                completionHandler(newAccessToken, nil)
            } catch {
                completionHandler(nil, error)
            }
        }
    }

    public func getValue(
        _ completionHandler: @escaping CommunicationTokenCompletionHandler
    ) {
        if !shouldRefresh() {
            completionHandler(currentToken, nil)
            return
        }

        anyThreadRefreshing.wait()
        DispatchQueue.global(qos: .utility).async { [weak self] in
            guard let self = self else { return }

            defer { self.anyThreadRefreshing.signal() }

            if !self.shouldRefresh() {
                completionHandler(self.currentToken, nil)
                return
            }

            self.refreshAccessToken { [weak self] accessToken, error in
                guard let self = self else { return }

                if error != nil {
                    completionHandler(nil, error)
                    return
                }

                self.currentToken = accessToken!
                completionHandler(self.currentToken, nil)
            }
        }
    }

    private func shouldRefresh() -> Bool {
        if currentToken.token.isEmpty {
            return true
        }

        let timeInterval = scheduleProactivelyRefreshing
            ? -proactiveRefreshingInterval
            : -onDemandRefreshingInterval

        return Date() >= currentToken.expiresOn.addingTimeInterval(timeInterval)
    }

    private func maybeScheduleRefresh() {
        if !scheduleProactivelyRefreshing {
            return
        }
        var actionPeriod = TimeInterval.zero
        if !isTokenExpired(accessToken: currentToken) {
            let now = Date()
            let tokenTtl = currentToken.expiresOn.timeIntervalSince(now)
            actionPeriod = shouldRefresh()
                ? tokenTtl / refreshAfterTTLDivider
                : tokenTtl - proactiveRefreshingInterval
        }
        proactiveRefreshTimer?.invalidate()
        proactiveRefreshTimer = Timer.scheduledTimer(withTimeInterval: actionPeriod, repeats: false) { [weak self] _ in
            self?.getValue { _, _ in }
        }
    }

    private func isTokenExpired(accessToken: CommunicationAccessToken?) -> Bool {
        return accessToken == nil || Date() >= accessToken!.expiresOn
    }

    public func cancel() {
        proactiveRefreshTimer?.invalidate()
    }

    deinit {
        proactiveRefreshTimer?.invalidate()
    }
}
