//
//  AsyncLoadingState.swift
//  Zapp-App
//
//  Created by Alex Zchut on 07/10/2021.
//  Copyright © 2021 Applicaster LTD. All rights reserved.
//

import Foundation
import XrayLogger

public class AsyncLoadingState {
    lazy var logger = Logger.getLogger(for: AsyncLoadingStateLogs.subsystem)

    /// Delegate instance uses to fetch state by id
    var delegate: AsyncLoadingStateMachineDelegate?
    /// Readable name for debugging
    var readableName: String {
        "\(name) \(origin)"
    }

    var name: String = ""
    var origin: String = ""

    /// UUID of the loading state
    let uuid = UUID().uuidString
    /// General  handler of the loading state.
    public var handler: (() async -> Result<Bool, AsyncOperationError>)?
    /// State dependancies that will be called after current state will finish it's work
    public var dependentStatesUUIDs: [String] = []

    public typealias ReturnType = (name: String?, result: Result<Bool, AsyncLoadingStateError>)

    public enum AsyncLoadingStateError: Error, CustomStringConvertible {
        case handlerNotDefined
        case handlerFailed
        case taskCancelled
        case dependentStatesFailed([String])
        case handlerFailedWithError(AsyncOperationError)

        public var description: String {
            var format = ""

            switch self {
            case .handlerNotDefined:
                format = NSLocalizedString("Handler not defined", comment: "")
            case .handlerFailed:
                format = NSLocalizedString("Handler failed", comment: "")
            case .taskCancelled:
                format = NSLocalizedString("Task cancelled", comment: "")
            case let .dependentStatesFailed(names):
                format = NSLocalizedString("Dependent states failed: \(names.joined(separator: ", "))", comment: "")
            case let .handlerFailedWithError(error):
                format = error.description
            }

            return String.localizedStringWithFormat(format)
        }
    }

    /// Defines state loading status
    var loadingState: LoadingStateType = .initial {
        didSet {
            DispatchQueue.main.async {
                self.logger?.debugLog(message: "\(AsyncLoadingStateLogs.loadingStateDidSet.message) \(self.readableName): \(self.loadingState.toString())",
                                      data: ["uuid": self.uuid,
                                             "readable_name": self.readableName,
                                             "loading_state": self.loadingState.toString()])
            }
        }
    }

    /// Force load state
    func load() async -> ReturnType {
        guard let handler = handler else {
            return (name: name, result: .failure(.handlerNotDefined))
        }

        guard [LoadingStateType.initial, LoadingStateType.loading].contains(loadingState) else {
            var result: Result<Bool, AsyncLoadingStateError> = .success(true)
            switch loadingState {
            case .failed:
                result = .failure(.handlerFailed)
            case .success:
                result = .success(true)
            default: break
            }

            // task handler call bypassed, already completed
            return (name: name, result: result)
        }

        if loadingState == .initial {
            loadingState = .loading

            let dependentStatesGroup = await withTaskGroup(of: ReturnType.self,
                                                           returning: [ReturnType].self, body: { taskGroup in
                                                               for stateUUID in dependentStatesUUIDs {
                                                                   if let state = delegate?.state(by: stateUUID) {
                                                                       taskGroup.addTask {
                                                                           await state.load()
                                                                       }
                                                                   }
                                                               }

                                                               var childStatesResults: [ReturnType] = []
                                                               for await response in taskGroup {
                                                                   childStatesResults.append(response)
                                                               }

                                                               return childStatesResults
                                                           })

            var result: Result<Bool, AsyncLoadingStateError> = .success(true)
            // get failed dependent states
            let filteredFailed = dependentStatesGroup.filter { stateReturnType in
                var retValue = false
                switch stateReturnType.result {
                case .failure:
                    retValue = true
                default: break
                }
                return retValue
            }

            // check if there are failed dependent states to fail parent step, set failure
            if filteredFailed.count > 0 {
                let names = filteredFailed.compactMap(\.name)
                result = .failure(.dependentStatesFailed(names))
                loadingState = .failed
            } else {
                switch await handler() {
                case .success:
                    loadingState = .success
                case let .failure(error):
                    loadingState = .failed
                    result = .failure(.handlerFailedWithError(error))
                }
            }

            return (name: name, result: result)
        } else {
            do {
                // task handler call posponed, waiting for completion
                try await Task.sleep(seconds: 0.15)
                return await load()
            } catch {
                return (name: name, result: .failure(.taskCancelled))
            }
        }
    }
}

extension Task where Success == Never, Failure == Never {
    /// Suspends the current task for at least the given duration in seconds.
    /// Throws if the task is cancelled while suspended.
    /// - Parameter seconds: The sleep duration in seconds.
    static func sleep(seconds: TimeInterval) async throws {
        try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
    }
}
