import React
import BlazeSDK

class BlazeWidgetBase<T: BlazeWidgetView>: RCTView {
    
    @objc var onWidgetDataLoadStartedInternal: RCTBubblingEventBlock?
    @objc var onWidgetDataLoadCompletedInternal: RCTBubblingEventBlock?
    @objc var onItemClickedInternal: RCTBubblingEventBlock?
    @objc var onWidgetPlayerDidAppearInternal: RCTBubblingEventBlock?
    @objc var onWidgetPlayerDismissedInternal: RCTBubblingEventBlock?
    @objc var onTriggerCTAInternal: RCTBubblingEventBlock?
    @objc var onTriggerPlayerBodyTextLinkInternal: RCTBubblingEventBlock?
    @objc var onPlayerEventTriggeredInternal: RCTBubblingEventBlock?
    @objc var onHeightChangedInternal: RCTBubblingEventBlock?
    
    private var observation: NSKeyValueObservation?
    
    weak var viewManager: RCTViewManager?
    
    lazy var widget: T = {
        return createWidget() as! T
    }()
    
    lazy var delegate: BlazeWidgetDelegate = .init(
        
        onDataLoadStarted: { [weak self] params in
            self?.onDataLoadStarted(playerType: params.playerType,
                                    sourceId: params.sourceId)
        },
        
        onDataLoadComplete: { [weak self] params in
            self?.onDataLoadComplete(playerType: params.playerType,
                                     sourceId: params.sourceId,
                                     itemsCount: params.itemsCount,
                                     result: params.result)
        },
        
        onPlayerDidAppear: { [weak self] params in
            self?.onPlayerDidAppear(playerType: params.playerType,
                                    sourceId: params.sourceId)
        },
        
        onPlayerDidDismiss: { [weak self] params in
            self?.onPlayerDidDismiss(playerType: params.playerType,
                                     sourceId: params.sourceId)
        },
        
        onTriggerCTA: { [weak self] params in
            self?.onTriggerCTA(playerType: params.playerType,
                               sourceId: params.sourceId,
                               actionType: params.actionType,
                               actionParam: params.actionParam) ?? false
        },
        
        onTriggerPlayerBodyTextLink: { [weak self] params in
            self?.onTriggerPlayerBodyTextLink(playerType: params.playerType,
                                              sourceId: params.sourceId,
                                              actionParam: params.actionParam) ?? .deeplink
        },
        
        onPlayerEventTriggered: { [weak self] params in
            self?.onPlayerEventTriggered(playerType: params.playerType,
                                         sourceId: params.sourceId,
                                         event: params.event)
        },
        
        onWidgetItemClicked: { [weak self] params in
            self?.onWidgetItemClicked(widgetId: params.widgetId,
                                      widgetItemId: params.widgetItemId,
                                      widgetItemTitle: params.widgetItemTitle)
        }
        
    )
    
    init(viewManager: RCTViewManager) {
        self.viewManager = viewManager
        
        super.init(frame: .zero)
        
        widget.widgetDelegate = delegate
        
        // Fix layout bugs of widgets with flex height defined - happens for both row and grid widgets.
        observation = widget.observe(
            \.bounds,
             options: [.old, .new]
        ) { [weak self] object, change in
            self?.updateWidgetSize(newSize: change.newValue?.size)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @objc var dataSource: [String : AnyHashable]? {
        didSet {
            guard let datasourceType = dataSource?.toDataSourceType else {
                return
            }
            widget.dataSourceType = datasourceType
        }
    }
    
    @objc var presetWidgetLayout: String?
    
    @objc var blazeWidgetLayout: [String : AnyHashable]?
    
    @objc var perItemStyleOverridesInternal: [String : AnyHashable]?
    
    @objc var appOverridesCTAHandling: Bool = false
    
    @objc var isEmbeddedInScrollView: Bool = false {
        didSet {
            widget.isEmbededInScrollView = isEmbeddedInScrollView
        }
    }
    
    @objc var shouldOrderWidgetByReadStatus: Bool = true
    
    @objc var cachingLevel: String?
    
    var didInitiateWidget: Bool = false
    override func didSetProps(_ changedProps: [String]!) {
        super.didSetProps(changedProps)
        
        guard !didInitiateWidget else {
            return
        }
        didInitiateWidget = true
        
        widget.shouldOrderWidgetByReadStatus = shouldOrderWidgetByReadStatus
        
        widget.cachePolicyLevel = cachingLevel?.asCachingLevel
        
        // All properties have been set, we add our widget to the view hierarchy and load the data.
        addViewAndLoad()
    }
    
    func addViewAndLoad() {
        if widget.superview == nil {
            // Add view.
            
            widget.translatesAutoresizingMaskIntoConstraints = false
            addSubview(widget)
            NSLayoutConstraint.activate([
                widget.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                widget.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                widget.topAnchor.constraint(equalTo: self.topAnchor)
            ])
            
            if widget.isEmbededInScrollView == false {
                widget.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
            }
        }
        
        setupWidgetLayout()
        
        setupPerItemStyleOverrides(perItemStyleOverridesInternal)
        
        widget.reloadLayout()
        widget.reloadData(progressType: .skeleton)
    }
    
    func setupWidgetLayout() {
        let layout: BlazeWidgetLayout? = parseLayout(from: presetWidgetLayout)
        
        guard let layout else { return }
        
        let customizedLayout = layout.mergedWith(blazeWidgetLayout?.toReactWidgetLayout)
        widget.layout = customizedLayout
    }
    
    // Function to convert and merge React Native styles to native styles
    func convertAndMergeOverrideStyles(_ perItemStyleOverridesRaw: [String: AnyHashable]?) -> [BlazeSDK.BlazeWidgetItemCustomMapping: BlazeSDK.BlazeWidgetItemStyleOverrides]? {
        
        guard let perItemStyleOverrides = perItemStyleOverridesRaw else {
            return nil
        }

        let customizationReactMap = perItemStyleOverrides.toReactWidgetStylesOverridesMap

        // Convert React Native styles to native styles and merge them
        let customizationMap = customizationReactMap?.compactMap { (key, reactOverrides) -> (BlazeSDK.BlazeWidgetItemCustomMapping, BlazeSDK.BlazeWidgetItemStyleOverrides)? in
            
            // Convert React key and value to native equivalents
            let nativeMapping = BlazeSDK.BlazeWidgetItemCustomMapping(key: key.key, value: key.value)
            
            var nativeOverrides = BlazeSDK.BlazeWidgetItemStyleOverrides(
                statusIndicator: widget.layout.widgetItemStyle.statusIndicator,
                imageBorder: widget.layout.widgetItemStyle.image.border,
                badge: widget.layout.widgetItemStyle.badge
            )
            
            // Merging the native values with the React overrides
            nativeOverrides = nativeOverrides.mergedWith(reactOverrides)
            
            return (nativeMapping, nativeOverrides)
        }

        // Convert tuple array to dictionary if not empty
        if let customizationMap = customizationMap, !customizationMap.isEmpty {
            return Dictionary(uniqueKeysWithValues: customizationMap)
        } else {
            return nil
        }
    }
    
    // Setup function that calls the conversion and merge function and applies the results
    func setupPerItemStyleOverrides(_ perItemStyleOverridesRaw: [String: AnyHashable]?) {
        // Call the function to convert and merge styles
        if let customizationMap = convertAndMergeOverrideStyles(perItemStyleOverridesRaw), !customizationMap.isEmpty {
            // Apply the resulting map to the widget
            widget.perItemStyleOverrides = customizationMap
        }
    }
    
    var currentWidgetHeightReported: Double?
    
    /// This function changes the view's height dynamically and foces it to layout with the right size.
    /// Without this Row widgets won't show with `flex` dimentions, and grid won't resize properly.
    ///
    /// - Parameter newSize: the new measured size of the widget.
    func updateWidgetSize(newSize: CGSize?) {
        guard let newSize,
              currentWidgetHeightReported != newSize.height else { return }
        
        currentWidgetHeightReported = newSize.height
        
        let params: [String : AnyHashable] = [
            "newHeight": newSize.height
        ]
        self.onHeightChangedInternal?(params)
    }
    
    func createWidget() -> BlazeWidgetView {
        fatalError("Subclasses must implement")
    }
    
    func parseLayout(from presetWidgetLayout: String?) -> BlazeWidgetLayout {
        fatalError("Subclasses must implement")
    }
    
}

extension BlazeWidgetBase: BlazeWidgetBaseProtocol {
    
    func reloadData(isSilentRefresh: Bool) {
        widget.reloadData(progressType: isSilentRefresh ? .silent : .skeleton)
    }
    
    func updateDataSource(dataSource: [String : AnyHashable]?, isSilentRefresh: Bool) {
        self.dataSource = dataSource
        widget.reloadData(progressType: isSilentRefresh ? .silent : .skeleton)
    }
    
    func play() {
        widget.play()
    }
    
    func updateOverrideStyles(perItemStyleOverrides: [String : AnyHashable]?, shouldUpdateUi: Bool) {
        
        // Call the function to convert and merge styles
        if let customizationMap = convertAndMergeOverrideStyles(perItemStyleOverrides), !customizationMap.isEmpty {
            // Apply the resulting map to the widget
            widget.updateOverrideStyles(stylesPerItem: customizationMap, shouldUpdateUI: shouldUpdateUi)
        }
    }
    
    func updateWidgetsUi() {
        widget.updateWidgetsUI()
    }
    
}

extension BlazeWidgetBase {
    
    func onDataLoadStarted(playerType: BlazePlayerType, sourceId: String?) {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId
        ]
        self.onWidgetDataLoadStartedInternal?(params)
    }
    
    func onDataLoadComplete(playerType: BlazePlayerType, sourceId: String?, itemsCount: Int, result: BlazeResult) {
        var params: [String : AnyHashable] = [
            "widgetId": sourceId,
            "itemsCount": itemsCount,
        ]
        
        if case .failure(let error) = result {
            params["error"] = error.errorMessage
        }
        
        self.onWidgetDataLoadCompletedInternal?(params)
    }
    
    func onPlayerDidAppear(playerType: BlazePlayerType, sourceId: String?) {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId
        ]
        self.onWidgetPlayerDidAppearInternal?(params)
    }
    
    func onPlayerDidDismiss(playerType: BlazePlayerType, sourceId: String?) {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId
        ]
        self.onWidgetPlayerDismissedInternal?(params)
    }
    
    func onWidgetItemClicked(widgetId: String, widgetItemId: String, widgetItemTitle: String?) {
        let params: [String : AnyHashable] = [
            "widgetId": widgetId,
            "widgetItemId": widgetItemId,
            "widgetItemTitle": widgetItemTitle
        ]
        self.onItemClickedInternal?(params)
    }
    
    func onTriggerCTA(playerType: BlazePlayerType, 
                      sourceId: String?,
                      actionType: BlazeCTAActionType,
                      actionParam: String) -> Bool {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId,
            "actionType": actionType.toReactValue,
            "actionParam": actionParam
        ]
        self.onTriggerCTAInternal?(params)
        
        return appOverridesCTAHandling
    }
    
    func onTriggerPlayerBodyTextLink(playerType: BlazePlayerType, sourceId: String?, actionParam: String) -> BlazeLinkActionHandleType {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId,
            "actionParam": actionParam
        ]
        self.onTriggerPlayerBodyTextLinkInternal?(params)
        
        return .deeplink
    }
    
    func onPlayerEventTriggered(playerType: BlazePlayerType, sourceId: String?, event: BlazePlayerEvent) {
        let params: [String : AnyHashable] = [
            "widgetId": sourceId,
            "playerEventType": event.toReactEventType,
            "playerEventParams": event.toReactEventParams
        ]
        self.onPlayerEventTriggeredInternal?(params)
    }
}
