import Foundation
import OpenTok
import React

@objc public class OpentokReactNativeImpl: NSObject {

    var ot: OpentokReactNative?

    @objc public init(ot: OpentokReactNative) {
        self.ot = ot
        super.init()
        OTRN.sharedState.opentokModule = ot
    }

    @objc public func initSession(
        _ apiKey: String, sessionId: String, sessionOptions: [String: Any]
    ) {
        guard !sessionId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            print("[OpentokReactNative] Invalid sessionId: sessionId is nil or empty. Session will not be created.")
            return
        }
        let enableStereoOutput: Bool = Utils.sanitizeBooleanProperty(
            sessionOptions["enableStereoOutput"] as Any)
        if enableStereoOutput == true {
            let customAudioDevice = OTCustomAudioDriver()
            OTAudioDeviceManager.setAudioDevice(customAudioDevice)
        }
        let settings = OTSessionSettings()
        settings.connectionEventsSuppressed = Utils.sanitizeBooleanProperty(
            sessionOptions["connectionEventsSuppressed"] as Any)
        settings.proxyURL = Utils.sanitizeStringProperty(
            sessionOptions["proxyUrl"] as Any)
        settings.ipWhitelist = Utils.sanitizeBooleanProperty(
            sessionOptions["ipWhitelist"] as Any)
        settings.iceConfig = Utils.sanitizeIceServer(
            sessionOptions["customServers"] as Any,
            sessionOptions["transportPolicy"] as Any,
            sessionOptions["filterOutLanCandidates"] as Any,
            sessionOptions["includeServers"] as Any)
        settings.singlePeerConnection = Utils.sanitizeBooleanProperty(
            sessionOptions["enableSinglePeerConnection"] as Any)
        settings.sessionMigration = Utils.sanitizeBooleanProperty(
            sessionOptions["sessionMigration"] as Any)
        let sessionDelegateHandler = SessionDelegateHandler(impl: self)

        OTRN.sharedState.sessionDelegateHandlers[sessionId] = sessionDelegateHandler
        let session = OTSession(
            apiKey: apiKey, sessionId: sessionId,
            delegate: sessionDelegateHandler, settings: settings)
        guard let session = session else {
            print("[OpentokReactNative] Failed to create OTSession for sessionId: \(sessionId)")
            OTRN.sharedState.sessionDelegateHandlers.removeValue(forKey: sessionId)
            return
        }
        
        // Set custom API URL if provided
        let apiUrlString = Utils.sanitizeStringProperty(sessionOptions["apiUrl"] as Any)
        if !apiUrlString.isEmpty, let apiUrl = URL(string: apiUrlString) {
            // Use the private setApiRootURL method available in OTSession
            if session.responds(to: Selector(("setApiRootURL:"))) {
                session.perform(Selector(("setApiRootURL:")), with: apiUrl)
            }
        }
        
        OTRN.sharedState.sessions.updateValue(session, forKey: sessionId)
    }

    @objc public func connect(
        _ sessionId: String,
        token: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        var error: OTError?
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error connecting to session. Could not find native session instance",
                nil)
            return
        }
        
        session.connect(withToken: token, error: &error)
        if let err = error {
            reject("ERROR", err.localizedDescription, err)
        } else {
            OTRN.sharedState.sessionConnectCallbacks[sessionId] = { _ in
                resolve(nil)
            }
        }
    }

    @objc public func disconnect(
        _ sessionId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        var error: OTError?
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error disconnecting from session. Could not find native session instance",
                nil)
            return
        }

        session.disconnect(&error)
        if let err = error {
            reject("ERROR", err.localizedDescription, err)
        } else {
            // Store resolve callback to be called after session fully disconnects
            OTRN.sharedState.sessionDisconnectCallbacks[sessionId] = { _ in
                resolve(nil)
            }
        }
    }

    @objc public func sendSignal(
        _ sessionId: String,
        signal: [String: String],
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        var error: OTError?
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error sending signal. Could not find native session instance",
                nil)
            return
        }

        if let connectionId = signal["to"] {
            let connection = OTRN.sharedState.connections[connectionId]
            session.signal(
                withType: signal["type"], string: signal["data"],
                connection: connection, error: &error)
        } else {
            session.signal(
                withType: signal["type"], string: signal["data"],
                connection: nil, error: &error)
        }

        if let err = error {
            reject("ERROR", err.localizedDescription, err)
        } else {
            resolve(nil)
        }
    }

    @objc public func setEncryptionSecret(
        _ sessionId: String,
        secret: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        var error: OTError?
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error setting encryption secret. Could not find native session instance.",
                nil)
            return
        }

        session.setEncryptionSecret(secret, error: &error)
        if let err = error {
            reject("ERROR", err.localizedDescription, nil)
        } else {
            resolve(nil)
        }
    }

    @objc public func getCapabilities(
        _ sessionId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error reporting issue. Could not find native session instance.",
                nil)
            return
        }
        var sessionCapabilities: Dictionary<String, Any> = [:];
        sessionCapabilities["canPublish"] = session.capabilities?.canPublish;
        // Bug in OT iOS SDK. This is set to false, but it should be true:
        sessionCapabilities["canSubscribe"] = true;
        sessionCapabilities["canForceMute"] = session.capabilities?.canForceMute;
        sessionCapabilities["canForceDisconnect"] = session.capabilities?.canForceDisconnect;
        resolve(sessionCapabilities);
    }

    @objc public func reportIssue(
        _ sessionId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject(
                "ERROR",
                "Error reporting issue. Could not find native session instance.",
                nil)
            return
        }

        var issueId: NSString?
        session.reportIssue(&issueId)

        if let id = issueId {
            resolve(id as String)
        } else {
            reject("ERROR", "Failed to generate issue ID", nil)
        }
    }

    @objc public func publish(_ sessionId: String, publisherId: String) {
        var error: OTError?

        guard let publisher = OTRN.sharedState.publishers[publisherId] else {
            return
        }

        guard let session = OTRN.sharedState.sessions[sessionId] else {
            return
        }

        session.publish(publisher, error: &error)
        // Handle error if needed
    }

    @objc public func unpublish(_ sessionId: String, publisherId: String) {
        var error: OTError?

        guard let publisher = OTRN.sharedState.publishers[publisherId] else {
            return
        }

        guard let session = OTRN.sharedState.sessions[sessionId] else {
            return
        }

        session.unpublish(publisher, error: &error)
    }

    @objc public func removeSubscriber(_ sessionId: String, streamId: String) {
        var error: OTError?

        guard let session = OTRN.sharedState.sessions[sessionId] else {
            return
        }

        guard let subscriber = OTRN.sharedState.subscribers[streamId] else {
            return
        }

        session.unsubscribe(subscriber, error: &error)
    }

    @objc public func forceMuteAll(
        _ sessionId: String,
        excludedStreamIds: [String],
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject("event_failure", "Session ID not found", nil)
            return
        }
        var excludedStreams: [OTStream] = []
        for streamId in excludedStreamIds {
            guard
                let stream = OTRN.sharedState.subscriberStreams[streamId]
                    ?? OTRN.sharedState.publisherStreams[streamId]
            else {
                continue  // Ignore bogus stream IDs
            }
            excludedStreams.append(stream)
        }
        var error: OTError?
        session.forceMuteAll(excludedStreams, error: &error)
        if let error = error {
            reject("event_failure", error.localizedDescription, nil)
            return
        }
        resolve(true)
    }

    @objc public func forceMuteStream(
        _ sessionId: String,
        streamId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject("event_failure", "Session ID not found", nil)
            return
        }
        guard
            let stream = OTRN.sharedState.subscriberStreams[streamId]
                ?? OTRN.sharedState.publisherStreams[streamId]
        else {
            reject("ERROR", "Stream ID not found", nil)
            return
        }
        var error: OTError?
        session.forceMuteStream(stream, error: &error)
        if let error = error {
            reject("event_failure", error.localizedDescription, nil)
            return
        }
        resolve(true)
    }

    @objc public func disableForceMute(
        _ sessionId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject("event_failure", "Session not found.", nil)
            return
        }
        var error: OTError?
        session.disableForceMute(&error)
        if let error = error {
            reject("event_failure", error.localizedDescription, nil)
            return
        }
        resolve(true)
    }

    @objc public func forceDisconnect(
        _ sessionId: String,
        connectionId: String,
        resolve: @escaping RCTPromiseResolveBlock,
        reject: @escaping RCTPromiseRejectBlock
    ) {
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            reject("event_failure", "Session ID not found", nil)
            return
        }
        guard let connection = OTRN.sharedState.connections[connectionId] else {
            reject("ERROR", "Connection ID not found", nil)
            return
        }
        var error: OTError?
        session.forceDisconnect(connection, error: &error)
        if let error = error {
            reject("event_failure", error.localizedDescription, nil)
            return
        }
        resolve(true)
    }

    @objc public func getPublisherRtcStatsReport(_ sessionId: String, publisherId: String) {
        guard let publisher = OTRN.sharedState.publishers[publisherId] else {
            return
        }
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            return
        }
        publisher.getRtcStatsReport()
        // If session context is needed, use session here
    }

    @objc public func getSubscriberRtcStatsReport(_ sessionId: String) -> Void {
        var error: OTError?
        guard let session = OTRN.sharedState.sessions[sessionId] else {
            return
        }
        for subscriber in OTRN.sharedState.subscribers {
            if let streamId = subscriber.value.stream?.streamId,
               OTRN.sharedState.subscriberStreams[streamId] != nil {
                subscriber.value.getRtcStatsReport(&error)
                if let error = error {
                    print("getSubscriberRtcStatsReport event_failure \(error.localizedDescription)")
                }
            }
        }
    }

    @objc public func setAudioTransformers(_ sessionId: String, publisherId: String, transformers: NSArray) -> Void {
        guard let publisher = OTRN.sharedState.publishers[publisherId] else {
            print("ERROR: Could not find publisher with ID \(publisherId)")
            return
        }
        // Optionally use sessionId for session-specific logic
        var nativeTransformers: [OTAudioTransformer] = []

        for case let transformer as [String: Any] in transformers {
            guard let transformerName = transformer["name"] as? String else {
                print("ERROR: Invalid transformer format. Each transformer must have a 'name' key")
                return
            }
            
            let transformerProperties = transformer["properties"] as? String ?? ""
            
            guard let nativeTransformer = OTAudioTransformer(
                name: transformerName,
                properties: transformerProperties
            ) else {
                print("ERROR: Failed to create audio transformer with name: \(transformerName)")
                return
            }
            
            nativeTransformers.append(nativeTransformer)
        }
        
        publisher.audioTransformers = nativeTransformers
    }

    @objc public func setVideoTransformers(_ sessionId: String, publisherId: String, transformers: NSArray) -> Void {
        guard let publisher = OTRN.sharedState.publishers[publisherId] else {
            print("ERROR: Could not find publisher with ID \(publisherId)")
            return
        }
        // Optionally use sessionId for session-specific logic
        var nativeTransformers: [OTVideoTransformer] = []

        for case let transformer as [String: Any] in transformers {
            guard let transformerName = transformer["name"] as? String else {
                print("ERROR: Invalid transformer format. Each transformer must have a 'name' key")
                return
            }
            
            let transformerProperties = transformer["properties"] as? String ?? ""
            
            guard let nativeTransformer = OTVideoTransformer(
                name: transformerName,
                properties: transformerProperties
            ) else {
                print("ERROR: Failed to create video transformer with name: \(transformerName)")
                return
            }
            
            nativeTransformers.append(nativeTransformer)
        }
        
        publisher.videoTransformers = nativeTransformers
    }
}

class DebugAlertHelper {
    class func showDebugAlert(message: String) {
        DispatchQueue.main.async {
            let alert = UIAlertController(
                title: "Debug", message: message, preferredStyle: .alert)
            alert.addAction(
                UIAlertAction(title: "OK", style: .default, handler: nil))
            if let rootViewController = UIApplication.shared.delegate?.window??
                .rootViewController
            {
                rootViewController.present(
                    alert, animated: true, completion: nil)
            }
        }
    }
}

private class SessionDelegateHandler: NSObject, OTSessionDelegate {
    weak var impl: OpentokReactNativeImpl?

    init(impl: OpentokReactNativeImpl) {
        self.impl = impl
        super.init()
    }

    public func sessionDidConnect(_ session: OTSession) {
        OTRN.sharedState.connections.updateValue(
            session.connection!, forKey: session.connection!.connectionId)
        // Multi-session: resolve promise callback if present
        if let callback = OTRN.sharedState.sessionConnectCallbacks[session.sessionId] {
            callback(nil)
            OTRN.sharedState.sessionConnectCallbacks.removeValue(forKey: session.sessionId)
        }
        let sessionInfo = EventUtils.prepareJSSessionEventData(session);
        impl?.ot?.emit(onSessionConnected: sessionInfo)
    }

    public func session(_ session: OTSession, didFailWithError error: OTError) {
        let errorInfo: [String: Any] = EventUtils.prepareJSErrorEventData(error)
        impl?.ot?.emit(onSessionError: errorInfo)
    }

    public func session(_ session: OTSession, streamCreated stream: OTStream) {
        OTRN.sharedState.subscriberStreams.updateValue(stream, forKey: stream.streamId)
        let streamInfo: [String: Any] = EventUtils.prepareJSStreamEventData(stream)
        impl?.ot?.emit(onStreamCreated: streamInfo)
        Utils.setStreamObservers(stream: stream, isPublisherStream: false)
    }

    public func session(_ session: OTSession, streamDestroyed stream: OTStream)
    {
        let streamInfo: [String: Any] = EventUtils.prepareJSStreamEventData(
            stream)
        OTRN.sharedState.subscriberStreams.removeValue(forKey: stream.streamId)
        OTRN.sharedState.subscribers.removeValue(forKey: stream.streamId)
        impl?.ot?.emit(onStreamDestroyed: streamInfo)
    }

    public func sessionDidDisconnect(_ session: OTSession) {
        let sessionInfo = EventUtils.prepareJSSessionEventData(session);
        // Multi-session: resolve promise callback if present
        if let callback = OTRN.sharedState.sessionDisconnectCallbacks[session.sessionId] {
            callback(nil)
            OTRN.sharedState.sessionDisconnectCallbacks.removeValue(forKey: session.sessionId)
        }
        impl?.ot?.emit(onSessionDisconnected: sessionInfo)

        session.delegate = nil
        OTRN.sharedState.sessions.removeValue(forKey: session.sessionId)
        OTRN.sharedState.sessionDelegateHandlers.removeValue(forKey: session.sessionId)
    }


    public func session(
        _ session: OTSession, connectionCreated connection: OTConnection
    ) {
        OTRN.sharedState.connections.updateValue(
            connection, forKey: connection.connectionId)
        var connectionInfo = EventUtils.prepareJSConnectionEventData(connection)
        connectionInfo["sessionId"] = session.sessionId;
        impl?.ot?.emit(onConnectionCreated: connectionInfo)
    }

    public func session(
        _ session: OTSession, connectionDestroyed connection: OTConnection
    ) {
        OTRN.sharedState.connections.removeValue(
            forKey: connection.connectionId)
        var connectionInfo = EventUtils.prepareJSConnectionEventData(connection)
        connectionInfo["sessionId"] = session.sessionId;
        impl?.ot?.emit(onConnectionDestroyed: connectionInfo)
    }
    public func session(_ session: OTSession, receivedSignalType type: String?, from connection: OTConnection?, with string: String?) {
        var signalData: Dictionary<String, Any> = [:];
        signalData["type"] = type;
        signalData["data"] = string;
        signalData["connectionId"] = connection?.connectionId;
        signalData["sessionId"] = session.sessionId;
        impl?.ot?.emit(onSignalReceived:  signalData)
    }
    public func session(_ session: OTSession, info muteForced: OTMuteForcedInfo)
    {
        var muteForcedInfo: [String: Any] = [:]
        muteForcedInfo["active"] = muteForced.active
        impl?.ot?.emit(onMuteForced: muteForcedInfo)
    }
    public func session(
        _ session: OTSession, archiveStartedWithId archiveId: String,
        name: String?
    ) {
        let archiveInfo: [String: Any] = [
            "archiveId": archiveId,
            "name": name ?? "",
            "sessionId": session.sessionId,
        ]
        impl?.ot?.emit(onArchiveStarted: archiveInfo)
    }

    public func session(
        _ session: OTSession, archiveStoppedWithId archiveId: String
    ) {
        let archiveInfo: [String: Any] = [
            "archiveId": archiveId,
            "name": "",  // Name is not available in stop event
            "sessionId": session.sessionId,
        ]
        impl?.ot?.emit(onArchiveStopped: archiveInfo)
    }
    public func sessionDidBeginReconnecting(_ session: OTSession) {
        let sessionInfo = EventUtils.prepareJSSessionEventData(session)
        impl?.ot?.emit(onSessionReconnecting: sessionInfo)
    }

    public func sessionDidReconnect(_ session: OTSession) {
        let sessionInfo = EventUtils.prepareJSSessionEventData(session)
        impl?.ot?.emit(onSessionReconnected: sessionInfo)
    }
}

