Untitled

mail@pastecode.io avatar
unknown
plain_text
2 years ago
32 kB
1
Indexable
Never
//
//  ScripDetailsViewModel.swift
//  UpstoxProBeta
//
//  Created by Cezar Carvalho on 2020-08-19.
//  Copyright © 2020 Upstox. All rights reserved.
//

import Foundation

protocol ScripDetailsViewModelInput {
  func viewDidLoad()
  func viewWillAppear()
  func viewWillDisappaear()
  func startDataListening()
  func stopDataListening()
  func updateData()
  func setHeaderVisibility(_ isVisible: Bool)
  func setTabIndex(_ index: Int, tabType: ScripDetailsTabsViewModel.Tab.TabType)
  func watchDidTap()
  func priceAlertsDidTap()
  func openOrderEntry(with orderSide: OrderSideModel, price: Double?, delegate: OrderEntryCollapseViewControllerDelegate)
  func enableSegmentDidTap()
  func buildStrategyDidTap()
  func pickStrategyDidTap()
  func gttDidTap(delegate: OrderEntryCollapseViewControllerDelegate)
  func doNavigateToStrategyStory()
  func doNavigateToPrediction(eventSource: LogEvent.StrategyOrderEvents.EventSource)
  func doNavigateToOptionChain()
  func doNavigateToTradeOptions()
  func scripDetailsScrollTableViewInsightsSection()
}

protocol ScripDetailsViewModelOutput {
  var tabsViewModel: ScripDetailsTabsViewModel { get }
  var useCustomNavigationBar: Bool { get }
  var title: String { get }
  var exchange: String { get }
  var expirationDate: Date? { get }
  var shortExchangeKeyword: String? { get }
  var navigationBarRightViewState: Observable<ScripDetailsNavigationBarRightView.State> { get }
  var watchState: Observable<DataLoadState<AnimatedSelection>> { get }
  var priceAlertsSelection: Observable<AnimatedSelection> { get }
  var primaryDataState: Observable<DataLoadState<ScripDetailsCoordinatorModels.PrimaryData>> { get }
  var orderEntryNeedsOpening: Observable<(side: OrderSideModel, price: Double?)?> { get }
  var bottomButtonTitleObservable: Observable<String> { get }
  var mode: ScripDetailsCoordinatorModels.Mode { get }
  func getTabIndex(of type: ScripDetailsTabsViewModel.Tab.TabType) -> Int
  var canShowMTFBanner: Observable<(canShow: Bool, scripSymbol: String)> { get }
}

protocol ScripDetailsViewModelDelegate: AnyObject {
  func reloadWatchlistData()
}

protocol ScripDetailsRoutingLogic: OpenWebURLRoutingLogic, OpenLeadModeRoutingLogic, ConditionalOrderOptionsRoutingLogic, ReactivationBottomSheetRoutingLogic {
  func routeToAddOrRemoveFromLists(delegate: AddRemoveListDelegate?)
  func routeToOrderEntry(with orderSide: OrderSideModel, price: Double?, collapsableDelegate: OrderEntryCollapseViewControllerDelegate?, selectedType: OrderEntryCategoryModel)
  func routeToPriceAlertDetails(with scripIdentifier: ScripIdentifier, delegate: PriceAlertsDetailsCoordinatorDelegate?)
  func routeToStrategyStory()
  func routeToPrediction(entryPoint: StrategyBuilderDataModel.OptionStrategyEntryPoint, eventSource: LogEvent.StrategyOrderEvents.EventSource)
  func routeToOptionChain(entryPoint: StrategyBuilderDataModel.OptionStrategyEntryPoint)
  func showConditionalOrderOptions(delegate: OrderEntryCollapseViewControllerDelegate)
  func routeToTutorialConfirmation()
  func routeToTradeOptions(delegate: TradeOptionsViewModelDelegate)
}

protocol ScripDetailsViewModelProtocol: ScripDetailsViewModelInput, ScripDetailsViewModelOutput, OpenLeadModeBottomSheetShowable { }

final class ScripDetailsViewModel: ScripDetailsViewModelProtocol {
  
  private let bodService: BODService
  private let profileWorker: ProfileWorkerProtocol?
  private let insightsWorker: InsightsWorkerProtocol
  private let watchedStateComputer: ExchangeWatchedStateComputable
  private let scripDetailsFeedSubscription: ScripDetailsFeedSubscription
  private let router: ScripDetailsRoutingLogic
  private let priceAlertsBellViewModel: PriceAlertsBellViewModel

  private let scripStateAccessQueue = DispatchQueue(label: "ScripDetailsViewModel#ScripState")
  private let headerRightButtonAccessQueue = DispatchQueue(label: "ScripDetailsViewModel#HeaderRightButton")
  private let dataUpdateQueue = DispatchQueue(label: "ScripDetailsViewModel#DataUpdate")
  private let primaryDataChangesQueue = DispatchQueue(label: "ScripDetailsViewModel#PrimaryDataChangesQueue")

  private let scripDetailsInfo: ScripDetailsInfo
  private let isHeaderShowed: Bool
  private let isHeaderRightButtonUpdatable: Bool
  private let showFourFractionalsForPrice: Bool
  private let isHeaderRightVisible: Bool
  private let eventSource: LogEvent.OrdersEvents.EventSource

  private var isAllDataUpdating = false
  private var isActiveTabDataSubscribed = false
  private var subscriptionTimeoutDuringDataUpdate = false
  weak var delegate: ScripDetailsViewModelDelegate?

  private var isWaitingForWatchlistLoadingNeeded = true
  private var isWaitingForFirstScripStatsNeeded: Bool
  private var isSegmentEnabled: Bool?
  private let mtfBannerViewModel: MTFBannerViewModelProtocol
  private let appServices: AppServices

  private var _scripState: ScripState?
  private var scripState: ScripState? {
    get {
      return scripStateAccessQueue.sync { _scripState }
    }
    set {
      scripStateAccessQueue.sync { _scripState = newValue }
    }
  }

  private var _headerRightButtonConfiguration: ScripDetailsCoordinatorModels.PrimaryData.Header.RightButtonConfiguration?
  private var headerRightButtonConfiguration: ScripDetailsCoordinatorModels.PrimaryData.Header.RightButtonConfiguration? {
    get {
      return headerRightButtonAccessQueue.sync { _headerRightButtonConfiguration }
    }
    set {
      headerRightButtonAccessQueue.sync { _headerRightButtonConfiguration = newValue }
    }
  }
  
  private var shouldShowMTFBanner: Bool {
    return scripDetailsInfo.isEquity && verifyIfMTFEnabled(for: scripDetailsInfo.identifier.token) && mtfBannerViewModel.canShowMTFBanner
  }
  
  private func verifyIfMTFEnabled(for token: String) -> Bool {
    return appServices.bodService.bodDataSource.checkIfMTFEnabledScripsExist(token: token)
  }

  private var stateUpdateSubscription: ReactiveDataSubscription?
  private var statsSubscription: ReactiveDataSubscription?
  
  let useCustomNavigationBar: Bool
  let title: String
  let exchange: String
  let expirationDate: Date?
  let shortExchangeKeyword: String?
  let mode: ScripDetailsCoordinatorModels.Mode

  let navigationBarRightViewState: Observable<ScripDetailsNavigationBarRightView.State> = Observable(.hidden)
  let watchState: Observable<DataLoadState<AnimatedSelection>> = Observable(.loading)
  var priceAlertsSelection: Observable<AnimatedSelection> { priceAlertsBellViewModel.selection }
  let primaryDataState: Observable<DataLoadState<ScripDetailsCoordinatorModels.PrimaryData>> = Observable(.loading)
  let orderEntryNeedsOpening: Observable<(side: OrderSideModel, price: Double?)?>
  lazy var bottomButtonTitleObservable: Observable<String> = Observable(scripDetailsInfo.bottomTitle)
  var canShowMTFBanner: Observable<(canShow: Bool, scripSymbol: String)> = Observable((false, ""))
  
  let tabsViewModel: ScripDetailsTabsViewModel
  
  init(router: ScripDetailsRouter,
       dataInput: ScripDetailsCoordinatorModels.DataInput,
       injections: ScripDetailsCoordinatorModels.Injections,
       bannerViewModel: MTFBannerViewModelProtocol,
       appServices: AppServices) {
    self.appServices = appServices
    self.mtfBannerViewModel = bannerViewModel
    self.router = router.scripDetailsRoutingComposition
    self.scripDetailsInfo = dataInput.scripDetailsInfo
    self.isHeaderShowed = dataInput.mode == .scripDetails
    appServices.userDefaultsStorage.bottomSheet = dataInput.mode == .bottomSheet
    self.isHeaderRightButtonUpdatable = dataInput.mode == .scripDetails && dataInput.scripDetailsInfo.identifier.exchange.isOptionChainAllowed
    self.mode = dataInput.mode

    self.bodService = injections.bodService
    self.profileWorker = injections.profileWorker
    self.watchedStateComputer = injections.watchedStateComputer
    self.insightsWorker = injections.insightsWorker

    self.scripDetailsFeedSubscription = ScripDetailsFeedSubscription(
      scripIdentifier: dataInput.scripDetailsInfo.identifier,
      config: .init(instrumentType: dataInput.scripDetailsInfo.instrument.type, mode: dataInput.mode),
      feedWorker: injections.feedWorker
    )

    let niftySymbol = String(scripDetailsInfo.symbol.prefix(6))
    let bankNiftySymbol = String(scripDetailsInfo.symbol.prefix(9))
    let nifty50Symbol = String(scripDetailsInfo.symbol.prefix(9))
    let niftyBank = String(scripDetailsInfo.symbol.prefix(11))
    appServices.userDefaultsStorage.scripIndexClicked = !(niftySymbol.caseInsensitiveCompare(OptionIndexSymbolModel.nifty.rawValue) == .orderedSame || bankNiftySymbol.caseInsensitiveCompare(OptionIndexSymbolModel.bankNifty.rawValue) == .orderedSame || nifty50Symbol.caseInsensitiveCompare(OptionIndexSymbolModel.nifty50.rawValue) == .orderedSame || niftyBank.caseInsensitiveCompare(OptionIndexSymbolModel.niftyBank.rawValue) == .orderedSame)

    self.isWaitingForFirstScripStatsNeeded = dataInput.mode == .scripDetails
    /// For Lead users the NSE_FO is by defalt Enabled
    if injections.userSession?.loggedUserType == .lead {
      isSegmentEnabled = true
    }
    self._headerRightButtonConfiguration = isHeaderRightButtonUpdatable ? nil : .hidden

    self.useCustomNavigationBar = dataInput.mode == .bottomSheet
    self.title = scripDetailsInfo.symbol.scripTitle(from: scripDetailsInfo.instrument.titleInfo)
    self.exchange = scripDetailsInfo.identifier.exchange.keywordValue
    self.expirationDate = {
      guard case .derivative(let info) = dataInput.scripDetailsInfo.instrument else { return nil }
      return info.expirationDate
    }()
    self.isHeaderRightVisible = dataInput.mode != .bottomSheet
    self.shortExchangeKeyword = scripDetailsInfo.symbol.scripShortExchangeKeyword(from: scripDetailsInfo.instrument.titleInfo)
    self.showFourFractionalsForPrice = scripDetailsInfo.isCurrencyDerivative

    self.priceAlertsBellViewModel = PriceAlertsBellViewModel(scripIdentifier: dataInput.scripDetailsInfo.identifier,
                                                             priceAlertsWorker: injections.priceAlertsWorker)

    let orderEntryNeedsOpening = Observable<(side: OrderSideModel, price: Double?)?>(nil)
    self.tabsViewModel = ScripDetailsTabsViewModel(router: router,
                                                   summaryInjections: injections.summary,
                                                   positionsWorker: injections.positionsWorker,
                                                   newsWorker: injections.summary.newsWorker,
                                                   chartsInjections: injections.chartsInjections,
                                                   scripDetailsFeedSubscription: scripDetailsFeedSubscription,
                                                   dataInput: dataInput,
                                                   selectedTabType: ScripDetailsTabsViewModel.Tab.TabType(rawValue: injections.userDefaultsStorage.lastVisitedScripDetailsTabBarType) ?? .summary) { side, price in
      orderEntryNeedsOpening.value = (side: side, price: price)
    }
    self.orderEntryNeedsOpening = orderEntryNeedsOpening
    eventSource = injections.eventSource
    setupInsights()
    setupFeedsSubscriptions()
  }

  deinit {
    stateUpdateSubscription?.unsubscribe()
    statsSubscription?.unsubscribe()
  }
  
  func viewDidLoad() {
    setupTabSubscription(userDefaultStorage: AppServices.shared.userDefaultsStorage)
    tabsViewModel.viewDidLoad()
    guard !appServices.userDefaultsStorage.conditionalOrdersTutorialWasShown else { return }
    router.routeToTutorialConfirmation()
    appServices.userDefaultsStorage.conditionalOrdersTutorialWasShown = true
  }
  
  func subscribeToMTFBannerMonitor() {
    appServices.mtfBannerMonitor.addObserver(self)
  }

  func viewWillAppear() {
    tabsViewModel.updateOptionStrategyBannerDisplay()
  }
  
  func viewWillDisappaear() {
    // this logic is important to reopen option details bottom sheet when user comes back after clicking on View chart or scrip name from that bottom sheet
    guard eventSource == .optionDetails else { return }
    appServices.userDefaultsStorage.lastVisitedScripDetailsTabBarType = ScripDetailsTabsViewModel.Tab.TabType.optionChain.rawValue
  }

  func startDataListening() {
    scripDetailsFeedSubscription.start()
  }

  func stopDataListening() {
    scripDetailsFeedSubscription.stop()
  }
  
  func updateData() {
    dataUpdateQueue.async {
      guard !self.isAllDataUpdating else { return }
      self.isAllDataUpdating = true
      DispatchQueue.main.sync {
        guard case .error = self.primaryDataState.value else { return }
        self.primaryDataState.value = .loading
        self.primaryDataChangesQueue.async {
          var subscriptionTypes: Set<ScripFeedDataType> = []
          if self.scripState == nil {
            subscriptionTypes.insert(.state)
          }
          if self.isWaitingForFirstScripStatsNeeded {
            subscriptionTypes.insert(.stats)
          }
          guard !subscriptionTypes.isEmpty else { return }
          self.scripDetailsFeedSubscription.retry(for: subscriptionTypes)
        }
      }

      func updateLast() {
        self.updateWatchlistItemState { isSuccess in
          guard isSuccess else { return self.allDataUpdateFailed() }
          self.primaryDataChangesQueue.async {
            guard self.isWaitingForWatchlistLoadingNeeded else { return }
            self.isWaitingForWatchlistLoadingNeeded = false
            self.tryUpdatePrimaryData()
          }
          self.updateInstrumentSpecificDataIfNeeded()
        }
      }

      guard self.isHeaderRightButtonUpdatable else { return updateLast() }
      DispatchQueue.main.async {
        let allExpiries = self.bodService.bodDataSource
          .getAllOptionExpiries(symbol: self.scripDetailsInfo.symbol.optionChainSymbol, exchange: self.scripDetailsInfo.identifier.exchange)
        let headerRightButtonConfiguration: ScripDetailsCoordinatorModels.PrimaryData.Header.RightButtonConfiguration = allExpiries.isEmpty ? .hidden : .optionChain
        self.tabsViewModel.optionChainShowValueChanged(shouldShow: headerRightButtonConfiguration == .optionChain)
        self.primaryDataChangesQueue.async {
          guard self.headerRightButtonConfiguration != headerRightButtonConfiguration else { return }
          self.headerRightButtonConfiguration = headerRightButtonConfiguration
          self.tryUpdatePrimaryData()
        }
        self.dataUpdateQueue.async { updateLast() }
      }
    }
  }

  func setHeaderVisibility(_ isVisible: Bool) {
    guard case .loaded = primaryDataState.value else { return }
    tryUpdateNavigationBarRightBarButtonItemsState()
  }
  
  func getTabIndex(of type: ScripDetailsTabsViewModel.Tab.TabType) -> Int {
    let activeTabsData = tabsViewModel.activeTabsData.value
    return activeTabsData?.tabs.firstIndex(where: { $0.type == type }) ?? 0
  }
  
  func setTabIndex(_ index: Int, tabType: ScripDetailsTabsViewModel.Tab.TabType) {
    appServices.userDefaultsStorage.lastVisitedScripDetailsTabBarType = tabType.rawValue
    tabsViewModel.changeSelectedIndex(to: UInt(index))
  }
  
  func scripDetailsScrollTableViewInsightsSection() {
    tabsViewModel.tabsScrollTableViewInsightsSection()
  }
  
  func watchDidTap() {
    router.routeToAddOrRemoveFromLists(delegate: self)
    trackScriptDetailsCTATap(.follow)
  }

  func priceAlertsDidTap() {
    guard !shouldShowLeadModeBottomSheet(for: .priceAlerts, router: router ) else { return }
    router.routeToPriceAlertDetails(with: scripDetailsInfo.identifier, delegate: priceAlertsBellViewModel)
    LogEvent.PriceAlerts.priceAlertEntryPoint = .scriptPage
    trackScriptDetailsCTATap(.alert)
  }

  func openOrderEntry(with orderSide: OrderSideModel, price: Double?, delegate: OrderEntryCollapseViewControllerDelegate) {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router),
          !shouldShowReactivationBottomSheet() else { return }
    router.routeToOrderEntry(with: orderSide, price: price, collapsableDelegate: delegate, selectedType: .regular)
    trackScriptDetailsCTATap(orderSide == .buy ? .buy : .sell)
  }
  
  func enableSegmentDidTap() {
    let clientInfo = AppServices.shared.cacheService.clientInfoCacheStore.getFromCache()
    let bottomSheetStyle = clientInfo?.getBottomSheetStyle(for: scripDetailsInfo.identifier.exchange)
    if let reactivationState = clientInfo?.reactivationState, let style = bottomSheetStyle {
      router.showReactivationBottomSheet(with: reactivationState, style: style)
    } else {
      router.routeToWebView(with: WebLinks.Profiles.Profile.Accounts.myTradingSegments)
    }
    trackScriptDetailsCTATap(.enableFutures)
  }
  
  func buildStrategyDidTap() {
    trackScriptDetailsCTATap(.buildStrategy)
  }

  func pickStrategyDidTap() {
    guard let activeTabs = tabsViewModel.activeTabsData.value?.tabs else { return }
    for (index, tab) in activeTabs.enumerated() {
      if tab.type == .strategy {
        tabsViewModel.changeSelectedIndex(to: UInt(index))
        trackScriptDetailsCTATap(.pickStrategy)
        return
      }
    }
  }
  
  func gttDidTap(delegate: OrderEntryCollapseViewControllerDelegate) {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router), !shouldShowReactivationBottomSheet() else { return }
    if scripDetailsInfo.isEquity {
      router.routeToOrderEntry(with: .buy, price: nil, collapsableDelegate: delegate, selectedType: .conditional)
    } else {
      router.showConditionalOrderOptions(delegate: delegate)
    }
    trackScriptDetailsCTATap(.gtt)
  }
  
}

// MARK: - AddRemoveListDelegate

extension ScripDetailsViewModel: AddRemoveListDelegate {
  
  func addRemoveListDidCreateList() { updateWatchlistItemState() }
  func addRemoveListDidCompleteWithChanges() {
    scripAdded(source: .scripDetails)
    updateWatchlistItemState()
    delegate?.reloadWatchlistData()
  }
  
}

// MARK: - TradeOptionsViewModelDelegate

extension ScripDetailsViewModel: TradeOptionsViewModelDelegate {
  
  func tradeOptionsViewModel(didSelectOption option: ScripDetailsCoordinatorModels.TradeOption) {
    switch option {
    case .pick:
      if appServices.userDefaultsStorage.optionStrategyStoryCompleted {
        router.routeToPrediction(entryPoint: .choose, eventSource: .scripNavigation)
      } else {
        router.routeToStrategyStory()
      }
    case .build:
      print("TODO")
      //TODO: add logic for build
    case .createGtt:
      router.routeToOrderEntry(with: .buy, price: nil, collapsableDelegate: nil, selectedType: .conditional)
    }
  }
  
}

// MARK: - SetupHelper

private typealias SetupHelper = ScripDetailsViewModel
private extension SetupHelper {
  
  func setupFeedsSubscriptions() {
    let stateUpdateSubscription = scripDetailsFeedSubscription
      .subscribe { [weak self] (result: Result<ScripState, ScripFeedError>) in
        guard let self = self else { return }
        switch result {
        case .success(let state):
          self.primaryDataChangesQueue.async {
            guard state != self.scripState else { return }
            self.scripState = state
            self.tryUpdatePrimaryData()
          }
        case .failure(let error):
          self.allDataUpdate(by: error)
        }
      }
    self.stateUpdateSubscription = stateUpdateSubscription
    guard isWaitingForFirstScripStatsNeeded else { return }
    self.primaryDataChangesQueue.async {
      let statsSubscription = self.scripDetailsFeedSubscription
        .subscribe { [weak self] (result: Result<ScripStats, ScripFeedError>) in
          guard let self = self else { return }
          switch result {
          case .success:
            self.primaryDataChangesQueue.async {
              self.statsSubscription?.unsubscribe()
              self.statsSubscription = nil
              self.isWaitingForFirstScripStatsNeeded = false
              self.tryUpdatePrimaryData()
            }
          case .failure(let error):
            self.allDataUpdate(by: error)
          }
        }
      self.statsSubscription = statsSubscription
    }
  }
  
  func setupTabSubscription(userDefaultStorage: UserDefaultsStorage) {
    tabsViewModel.activeTabsData.observe(on: self) { [weak self] activeTabsData in
      let selectedIndex = activeTabsData?.selectedIndex
      guard let self = self,
            let activeTabsData = activeTabsData,
            let selectedIndex = selectedIndex,
            selectedIndex >= 0,
            activeTabsData.tabs.count > selectedIndex else { return }
      let tabTitle = activeTabsData.tabs[Int(selectedIndex)].eventTabTitle
      if self.isActiveTabDataSubscribed {
        self.trackTabSwitched(tabViewed: tabTitle)
      } else {
        self.trackScriptDetailsViewed(tabViewed: tabTitle)
      }
      self.isActiveTabDataSubscribed = true
    }
  }
  
}

// MARK: - UpdateDataHelper

private typealias UpdateDataHelper = ScripDetailsViewModel
private extension UpdateDataHelper {

  func updateWatchlistItemState(completion: ((Bool) -> Void)? = nil) {
    DispatchQueue.main.async {
      self.watchState.setLoadingIfErrorOnly()
    }
    watchedStateComputer.getExchangeWatchedState(for: scripDetailsInfo.identifier) { [weak self] result in
      guard let self = self else { return }
      switch result {
      case .success(let exchangeWatchedState):
        DispatchQueue.main.async {
          switch self.watchState.value {
          case .loading, .error:
            self.watchState.value = .loaded(data: .init(
              selected: exchangeWatchedState,
              animated: .nonActive
            ))
          case .loaded(let data):
            self.watchState.value = .loaded(data: .init(
              selected: exchangeWatchedState,
              animated: data.selected != exchangeWatchedState ? .active : .nonActive
            ))
          }
        }
        completion?(true)
      case .failure:
        DispatchQueue.main.async {
          self.watchState.setErrorIfLoading()
        }
        completion?(false)
      }
    }
  }

  func allDataUpdateFailed() {
    self.dataUpdateQueue.async {
      DispatchQueue.main.sync {
        self.primaryDataState.setErrorIfLoading()
      }
      self.isAllDataUpdating = false
      self.subscriptionTimeoutDuringDataUpdate = false
    }
  }

  func allDataUpdateSuccessfully() {
    self.dataUpdateQueue.async {
      if self.subscriptionTimeoutDuringDataUpdate {
        self.subscriptionTimeoutDuringDataUpdate = false
        DispatchQueue.main.sync {
          self.primaryDataState.setErrorIfLoading()
        }
      }
      self.isAllDataUpdating = false
    }
  }

  func allDataUpdate(by error: ScripFeedError) {
    self.dataUpdateQueue.async {
      switch error {
      case .timeout:
        guard !self.isAllDataUpdating else {
          self.subscriptionTimeoutDuringDataUpdate = true
          return
        }
        DispatchQueue.main.sync {
          self.primaryDataState.setErrorIfLoading()
        }
      }
    }
  }

  func updateInstrumentSpecificDataIfNeeded() {
    guard let profileWorker = profileWorker else { return allDataUpdateSuccessfully() }
    profileWorker.getClientInfo(cachePolicy: .returnCacheDataElseLoad) { [weak self] result in
      guard let self = self else { return }
      var isSegmentEnabled: Bool
      switch result {
      case .success(let clientInfo):
        switch self.scripDetailsInfo.instrument {
        case .index:
          isSegmentEnabled = clientInfo.exchanges.contains(Exchange.nseFO.rawValue)
        case .derivative, .equity:
          isSegmentEnabled = clientInfo.exchanges.contains(self.scripDetailsInfo.identifier.exchange.rawValue)
        }
        self.primaryDataChangesQueue.async {
          guard self.isSegmentEnabled != isSegmentEnabled else { return }
          self.isSegmentEnabled = isSegmentEnabled
          self.tryUpdatePrimaryData()
        }
        self.allDataUpdateSuccessfully()
      case .failure:
        self.allDataUpdateFailed()
      }
    }
  }

  /// should be called on main thread
  func tryUpdateNavigationBarRightBarButtonItemsState() {
    let navigationBarRightViewState: ScripDetailsNavigationBarRightView.State = .scripState
    guard self.navigationBarRightViewState.value != navigationBarRightViewState, isHeaderRightVisible == true else { return }
    self.navigationBarRightViewState.value = navigationBarRightViewState
  }

  /// should be called on primary data queue
  func tryUpdatePrimaryData() {
    guard !isWaitingForWatchlistLoadingNeeded,
          !isWaitingForFirstScripStatsNeeded,
          let scripState = scripState,
          let headerRightButtonConfiguration = headerRightButtonConfiguration else { return }

    var bottomButtonsConfiguration: ScripDetailsCoordinatorModels.BottomButtonsConfiguration
    let equityCompanyName: String?
    switch scripDetailsInfo.instrument {
    case .index:
      equityCompanyName = nil
      let isSegmentDisabled = !(isSegmentEnabled ?? false)
      bottomButtonsConfiguration = scripDetailsInfo.isIndexNiftyBankNifty ? .indicesNiftyAndBankNifty(disabled: isSegmentDisabled) : .otherIndices
    case .equity(let equityData):
      equityCompanyName = equityData.issuerName
      bottomButtonsConfiguration = .equity
    case .derivative:
      guard let isSegmentEnabled = isSegmentEnabled else { return }
      if isSegmentEnabled {
        bottomButtonsConfiguration = scripDetailsInfo.isFNONiftyBankNifty ? .fnoNiftyAndBankNifty(disabled: !isSegmentEnabled) : .otherFNO(disabled: !isSegmentEnabled)
      } else {
        bottomButtonsConfiguration = scripDetailsInfo.isFNONiftyBankNifty ? .fnoNiftyAndBankNifty(disabled: !isSegmentEnabled): .enableFuturesAndOptions(segment: scripDetailsInfo.identifier.exchange)
      }
      equityCompanyName = nil
    }

    if mode == .bottomSheet {
      guard let isSegmentEnabled = isSegmentEnabled else { return }
      bottomButtonsConfiguration = !isSegmentEnabled ? .enableFuturesAndOptions(segment: scripDetailsInfo.identifier.exchange) : .bottomSheetOptionChain
    }

    let primaryData = ScripDetailsCoordinatorModels.PrimaryData(
      scripState: scripState,
      header: isHeaderShowed ? .showed(equityCompanyName: equityCompanyName,
                                       rightButtonConfiguration: headerRightButtonConfiguration) : .hidden,
      bottomButtonsConfiguration: bottomButtonsConfiguration,
      showFourFractionalsForPrice: showFourFractionalsForPrice,
      showPriceCurrencySymbol: !scripDetailsInfo.identifier.exchange.isIndex
    )

    DispatchQueue.main.async {
      self.tryUpdateNavigationBarRightBarButtonItemsState()
      self.primaryDataState.value = .loaded(data: primaryData)
    }
  }

}

// MARK: - ScripDetailsFeedSubscription.Config

private extension ScripDetailsFeedSubscription.Config {

  init(instrumentType: ScripInstrumentType, mode: ScripDetailsCoordinatorModels.Mode) {
    switch instrumentType {
    case .index:
      self = .index
    case .equity:
      self = .equity(includeDepth: mode == .scripDetails)
    case .derivative(let derivativeType):
      self = .derivative(includeDepth: mode == .scripDetails, includeGreeks: derivativeType == .option)
    }
  }

}

// MARK: - StrategyNavigationHelper

typealias StrategyNavigationHelper = ScripDetailsViewModel
extension StrategyNavigationHelper {
  
  func doNavigateToStrategyStory() {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router) else { return }
    router.routeToStrategyStory()
  }

  func doNavigateToPrediction(eventSource: LogEvent.StrategyOrderEvents.EventSource) {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router) else { return }
    router.routeToPrediction(entryPoint: .choose, eventSource: eventSource)
  }

  func doNavigateToOptionChain() {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router) else { return }
    router.routeToOptionChain(entryPoint: .create)
  }

  func doNavigateToTradeOptions() {
    guard !shouldShowLeadModeBottomSheet(for: .scriptDetails, router: router) else { return }
    appServices.userDefaultsStorage.moreButtonClicked = true
    router.routeToTradeOptions(delegate: self)
  }
  
}

// MARK: - ScripDetailsInfo

private extension ScripDetailsInfo {
  
  var bottomTitle: String {
    return isEquity ? "create_gtt_order".localized : "more_options".localized
  }
  
}

// MARK: - Insights

private typealias Insights = ScripDetailsViewModel
private extension Insights {
  
  func setupInsights() {
    guard scripDetailsInfo.instrument.type == ScripInstrumentType.equity else {
      tryUpdatePrimaryData()
      return
    }
    insightsWorker.getInsights(symbol: self.scripDetailsInfo.symbol) { [weak self] result in
      guard let self = self else { return }
      switch result {
      case .success(let insightsData):
        self.primaryDataChangesQueue.async {
          guard !(insightsData.description.isEmpty) else {
            self.tryUpdatePrimaryData()
            return }
          self.tryUpdatePrimaryData()
          self.tabsViewModel.updateInsightsDataInTab(insightsString: insightsData.description)
        }
        self.allDataUpdateSuccessfully()
      case .failure:
        self.tryUpdatePrimaryData()
      }
    }
  }
}

// MARK: Analytics

private typealias AnalyticHelper = ScripDetailsViewModel
private extension AnalyticHelper {

  func trackScriptDetailsViewed(tabViewed: LogEvent.ScriptDetails.TabTitle) {
    let logData = LogEvent.ScriptDetails.ScriptDetailsViewedLogData(scripName: title, tabName: tabViewed, source: LogEvent.ScriptDetails.scriptDetailSourceName)
    AppServices.shared.analyticsManager.trackEvent(LogEvent.ScriptDetails.scriptDetailsViewed(logData: logData))
  }
  
  func trackTabSwitched(tabViewed: LogEvent.ScriptDetails.TabTitle) {
    let logData = LogEvent.ScriptDetails.ScriptDetailsTabSwitchLogData(tabName: tabViewed)
    AppServices.shared.analyticsManager.trackEvent(LogEvent.ScriptDetails.scriptDetailsTabSwitched(logData: logData))
  }
  
  func trackScriptDetailsCTATap(_ buttonTitle: LogEvent.ScriptDetails.CTATitle) {
    let logData = LogEvent.ScriptDetails.ScriptDetailsCTATappedLogData(buttonName: buttonTitle)
    AppServices.shared.analyticsManager.trackEvent(LogEvent.ScriptDetails.scriptDetailsCTATapped(logData: logData))
  }

  func scripAdded(source: LogEvent.MyList.AddScripSource) {
    let logData = LogEvent.MyList.WatchlistAddScripLogData(source: source)
    appServices.analyticsManager.trackEvent(LogEvent.MyList.watchlistAddScrip(logData: logData))
  }
  
  func trackMTFActivated() {
    appServices.analyticsManager.trackEvent(LogEvent.MTFActivation.scripDetail)
  }
  
}

// MARK: - MTFBannerMonitorListener

extension ScripDetailsViewModel: MTFBannerMonitorListener {
  
  func removeMTFBanner() {
    canShowMTFBanner.value = (false, scripDetailsInfo.symbol)
  }
  
}

// MARK: - Helper

private typealias Helper = ScripDetailsViewModel
private extension Helper {
  
  func shouldShowReactivationBottomSheet() -> Bool {
    let clientInfo = appServices.cacheService.clientInfoCacheStore.getFromCache()
    guard let reactivationState = clientInfo?.reactivationState,
          let style = clientInfo?.getBottomSheetStyle(for: scripDetailsInfo.identifier.exchange) else { return false }
    router.showReactivationBottomSheet(with: reactivationState, style: style)
    return true
  }
  
}