import Foundation
import UIKit
import DevRevSDK
import React

@objc(DevRevSDK)
class DevRevSDK: RCTEventEmitter {
	// MARK: - Error type

	enum Error: Swift.Error {
		case couldNotResolveViewForTag
	}

	// MARK: - Properties

	private let jsonSanitizer: JSONSanitizer

	// MARK: - Lifecycle

	override init() {
		jsonSanitizer = .init()

		super.init()
	}

	// MARK: - Configuration

	@objc(configure:version:featureConfiguration:)
	public func configure(
		appID: String,
		version: String,
		featureConfiguration: [String: Any]?
	) {
		if let configDict = featureConfiguration as? [String: Any] {
			let config = FeatureConfigurationParser.parseFeatureConfiguration(from: configDict)
			DevRev.configure(appID: appID, featureConfiguration: config)
		} else {
			DevRev.configure(appID: appID)
		}
		DevRevInterop.setAppFrameworkInfo(name: "React Native", version: version)
	}

	@objc(updateFeatureConfiguration:)
	public func updateFeatureConfiguration(_ featureConfiguration: [String: Any]) {
		let config = FeatureConfigurationParser.parseFeatureConfiguration(from: featureConfiguration)
		DevRevInterop.updateFeatureConfiguration(config)
	}

	// MARK: - In-app link handling

	@objc(setShouldDismissModalsOnOpenLink:)
	public func setShouldDismissModalsOnOpenLink(_ shouldDismissModalsOnOpenLink: Bool) {
		DevRevInterop.shouldDismissModalsOnOpenLink = shouldDismissModalsOnOpenLink
	}

	// MARK: - Dynamic theme configuration

	@objc(setPrefersSystemTheme:)
	public func setPrefersSystemTheme(_ prefersSystemTheme: Bool) {
		DevRevInterop.prefersSystemTheme = prefersSystemTheme
	}

	// MARK: - Identification

	@objc(identifyUnverifiedUser:organizationID:)
	public func identifyUnverifiedUser(
		userID: String,
		organizationID: String?
	) {
		DevRevInterop.identifyUnverifiedUser(
			.init(
				userID: userID,
				organizationID: organizationID
			)
		)
	}

	@objc(identifyVerifiedUser:sessionToken:)
	public func identifyVerifiedUser(
		userID: String,
		sessionToken: String
	) {
		DevRevInterop.identifyVerifiedUser(userID, sessionToken: sessionToken)
	}

	@objc(identifyAnonymousUser:)
	public func identifyAnonymousUser(
		userID: String = UUID().uuidString
	) {
		DevRevInterop.identifyAnonymousUser(userID: userID)
	}

	@objc(updateUser:)
	public func updateUser(_ identity: [String: Any]) {
		let parsedIdentity = RCTConvert.identity(identity)

		// Extract all customFields from traits for sanitization.
		let userCustomFields = parsedIdentity.userTraits?.customFields
		let orgCustomFields = parsedIdentity.organizationTraits?.customFields
		let accountCustomFields = parsedIdentity.accountTraits?.customFields

		// Create sanitized versions of each customFields dictionary if they exist.
		let sanitizedUserCustomFields = userCustomFields.map(jsonSanitizer.sanitize)
		let sanitizedOrgCustomFields = orgCustomFields.map(jsonSanitizer.sanitize)
		let sanitizedAccountCustomFields = accountCustomFields.map(jsonSanitizer.sanitize)

		// Prepare a new set of user traits.
		let newUserTraits: Identity.UserTraits? = parsedIdentity.userTraits.map { traits in
				.init(
					displayName: traits.displayName,
					email: traits.email,
					fullName: traits.fullName,
					userDescription: traits.userDescription,
					phoneNumbers: traits.phoneNumbers,
					customFields: sanitizedUserCustomFields
				)
		}

		// Prepare a new set of organization traits.
		let newOrgTraits: Identity.OrganizationTraits? = parsedIdentity.organizationTraits.map { traits in
				.init(
					displayName: traits.displayName,
					domain: traits.domain,
					organizationDescription: traits.organizationDescription,
					phoneNumbers: traits.phoneNumbers,
					tier: traits.tier,
					customFields: sanitizedOrgCustomFields
				)
		}

		// Prepare a new set of account traits.
		let newAccountTraits: Identity.AccountTraits? = parsedIdentity.accountTraits.map { traits in
				.init(
					displayName: traits.displayName,
					domains: traits.domains,
					accountDescription: traits.accountDescription,
					phoneNumbers: traits.phoneNumbers,
					websites: traits.websites,
					tier: traits.tier,
					customFields: sanitizedAccountCustomFields
				)
		}

		// Create an updated identity with sanitized traits.
		let updatedIdentity = Identity(
			userID: parsedIdentity.userID,
			organizationID: parsedIdentity.organizationID,
			accountID: parsedIdentity.accountID,
			userTraits: newUserTraits,
			organizationTraits: newOrgTraits,
			accountTraits: newAccountTraits
		)

		DevRevInterop.updateUser(updatedIdentity)
	}

	@objc(logout:)
	public func logout(_ deviceID: String) {
		DevRevInterop.logout(deviceID: deviceID)
	}

	// MARK: - Support

	/// Presents the support feature modally.
	///
	/// - Important: Due to limitations in bridging React Native views to UIKit view controllers, this function will use
	/// the application's root view controller.
	@objc
	public func showSupport() {
		Task { @MainActor in
			guard
				let rootViewController = UIApplication.shared.delegate?.window??.rootViewController
			else {
				return
			}

			DevRevInterop.showSupport(
				from: rootViewController,
				isAnimated: true
			)
		}
	}

	/// Creates a new support conversation over the top most view controller.
	///
	/// - Important: Due to limitations in bridging React Native views to UIKit view controllers, this function will use
	/// the application's root view controller.
	@objc(createSupportConversation:)
	public func createSupportConversation(_ prefillMessage: String?) {
		Task { @MainActor in
			if let message = prefillMessage, !message.isEmpty {
				DevRevInterop.createSupportConversation(isAnimated: true, prefillMessage: message)
			} else {
				DevRevInterop.createSupportConversation(isAnimated: true)
			}
		}
	}

	// MARK: - Analytics

	@objc(trackEvent:properties:)
	public func trackEvent(
		name: String,
		properties: [String: String]?
	) {
		DevRevInterop.trackEvent(
			name: name,
			properties: properties ?? [:]
		)
	}

	// MARK: - User recording

	@objc
	public func startRecording() {
		DevRevInterop.startRecording()
	}

	@objc
	public func stopRecording() {
		DevRevInterop.stopRecording()
	}

	@objc
	public func pauseRecording() {
		DevRevInterop.pauseRecording()
	}

	@objc
	public func resumeRecording() {
		DevRevInterop.resumeRecording()
	}

	@objc
	public func pauseUserInteractionTracking() {
		DevRevInterop.pauseUserInteractionTracking()
	}

	@objc
	public func resumeUserInteractionTracking() {
		DevRevInterop.resumeUserInteractionTracking()
	}

	@objc
	public func processAllOnDemandSessions() {
		DevRevInterop.processAllOnDemandSessions()
	}

	// MARK: - Session properties

	@objc(addSessionProperties:)
	public func addSessionProperties(
		_ properties: [String: Any]
	) {
		DevRevInterop.addSessionProperties(
			properties.compactMapValues { $0 as? String }
		)
	}

	@objc
	public func clearSessionProperties() {
		DevRevInterop.clearSessionProperties()
	}

	// MARK: - Sensitive field masking

	@objc(markSensitiveViews:)
	public func markSensitiveViews(_ tags: [Any]) {
		sensitiveViews(tags) {
			DevRevInterop.markSensitiveViews($0)
		}
	}

	@objc(unmarkSensitiveViews:)
	public func unmarkSensitiveViews(_ tags: [Any]) {
		sensitiveViews(tags) { sensitiveViews in
			DevRevInterop.unmarkSensitiveViews(sensitiveViews)
		}
	}

	// MARK: - UIKit view resolution

	/// A bridge between the React Native and the UIKit view hierarchies.
	///
	/// The method iterates over the tags and extracts the view for each tag.
	/// The views are then passed to the native SDK for masking.
	///
	/// - Parameter tags: An array of views that should be masked.
	private func sensitiveViews(
		_ tags: [Any],
		apply: @escaping ([UIView]) -> Void
	) {
		Task { @MainActor in
			do {
				let views = try await tags
					.compactMap(number)
					.asyncThrowingMap { [weak self] in
						try await self?.view(for: $0)
					}
					.compactMap { $0 }

				let allSubViews = views.flatMap { findAllSubviews($0) }

				apply(allSubViews)
			}
			catch {
				print("Could not map the sensitive views! \(error)")
			}
		}
	}

	/// Finds all views including the root view and all its descendants.
	///
	/// - Parameter view: A root view.
	/// - Returns: An array of views.
	@MainActor
	private func findAllSubviews(_ view: UIView) -> [UIView] {
		[view] + view.subviews.flatMap { findAllSubviews($0) }
	}

	/// Resolves the UIKit view for the given React Native tag.
	///
	/// - Parameter tag: A tag number.
	/// - Returns: A view for the given tag.
	private func view(
		for tag: NSNumber
	) async throws -> UIView? {
		try await withCheckedThrowingContinuation { continuation in
			RCTGetUIManagerQueue().async { [weak self] in
				self?.bridge.uiManager.addUIBlock { manager, viewRegistry in
					guard
						let view = viewRegistry?[tag]
					else {
						continuation.resume(throwing: Error.couldNotResolveViewForTag)
						return
					}

					continuation.resume(returning: view)
				}
			}
		}
	}

	/// Extracts the number from the raw view tag object.
	///
	/// - Parameter tag: A raw view tag object.
	///
	/// - Returns: A number extracted from the tag object.
	private func number(for tag: Any) -> NSNumber? {
		if let tag = tag as? [String: Any] {
			return tag["_nativeTag"] as? NSNumber
		}

		if let tag = tag as? NSNumber {
			return tag
		}

		return nil
	}

	// MARK: - Timer control


	@objc(startTimer:properties:)
	public func startTimer(
		_ name: String,
		properties: [String: String] = [:]
	) {
		DevRevInterop.startTimer(
			name,
			properties: properties
		)
	}

	@objc(endTimer:properties:)
	public func endTimer(
		_ name: String,
		properties: [String: String] = [:]
	) {
		DevRevInterop.endTimer(
			name,
			properties: properties
		)
	}

	// MARK: - Monitoring

	@objc
	public func resumeAllMonitoring() {
		DevRevInterop.resumeAllMonitoring()
	}

	@objc
	public func stopAllMonitoring() {
		DevRevInterop.stopAllMonitoring()
	}

	// MARK: - Screen tracking

	@objc(trackScreenName:)
	public func trackScreenName(_ screenName: String) {
		DevRevInterop.trackScreenName(screenName)
	}

	// MARK: - Push notifications

	@objc(registerDeviceToken:deviceID:)
	public func registerDeviceToken(
		_ deviceToken: String,
		deviceID: String
	) {
		DevRevInterop.registerDeviceToken(
			deviceToken,
			deviceID: deviceID
		)
	}

	@objc(unregisterDevice:)
	public func unregisterDevice(_ deviceID: String) {
		DevRevInterop.unregisterDevice(deviceID)
	}

	@objc(processPushNotification:)
	public func processPushNotification(_ payload: String) {
		DevRevInterop.processPushNotification(payload)
	}

	// MARK: - Error capture

	@objc(captureError:tag:stackTrace:)
	public func captureError(
		_ error: String,
		tag: String,
		stackTrace: String?
	) {
		var userInfo: [String: Any] = [NSLocalizedDescriptionKey: error]
		let callStackSymbols = Thread.callStackSymbols

		if let stackTrace = stackTrace, !stackTrace.isEmpty {
			let stackTraceLines = stackTrace
				.components(separatedBy: "\n")
				.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
			userInfo["NSCallStackSymbols"] = stackTraceLines
		}
		else {
			userInfo["NSCallStackSymbols"] = callStackSymbols
		}

		let nsError = NSError(
			domain: "DevRevSDK",
			code: 0,
			userInfo: userInfo
		)
		DevRev.captureError(nsError, tag: tag)
	}
}
