import Capacitor
import CoreNFC
import UIKit

func nfcSessionEndReason(for error: Error) -> String? {
    switch (error as NSError).code {
    case NFCReaderError.readerSessionInvalidationErrorFirstNDEFTagRead.rawValue:
        return nil
    case NFCReaderError.readerSessionInvalidationErrorUserCanceled.rawValue:
        return "userCancelled"
    case NFCReaderError.readerSessionInvalidationErrorSessionTimeout.rawValue:
        return "sessionTimeout"
    default:
        return "invalidated"
    }
}

@objc(NfcPlugin)
public class NfcPlugin: CAPPlugin, CAPBridgedPlugin {
    private let pluginVersion: String = "8.2.2"
    static let defaultIosPollingOptions = ["iso14443", "iso15693"]

    public let identifier = "NfcPlugin"
    public let jsName = "CapacitorNfc"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "startScanning", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "stopScanning", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "write", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "erase", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "makeReadOnly", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "share", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "unshare", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "showSettings", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "isSupported", returnType: CAPPluginReturnPromise)
    ]

    private var ndefReaderSession: NFCNDEFReaderSession?
    private var tagReaderSession: NFCTagReaderSession?
    private let sessionQueue = DispatchQueue(label: "app.capgo.nfc.session")
    private var currentTag: NFCNDEFTag?
    private var invalidateAfterFirstRead = true
    private var sessionType: String = "ndef"
    private var pendingStartCall: CAPPluginCall?
    private var pendingStartSession: NFCTagReaderSession?
    private var pendingAlertMessage: String?
    private var tagSessionActivated = false
    private var tagSessionTriedFallback = false
    private var tagSessionPollingOptions: NFCTagReaderSession.PollingOption = []

    private func isSessionAvailable(for type: String) -> Bool {
        if type == "tag" {
            return NFCTagReaderSession.readingAvailable
        }
        return NFCNDEFReaderSession.readingAvailable
    }

    private func isNfcAvailable() -> Bool {
        NFCTagReaderSession.readingAvailable || NFCNDEFReaderSession.readingAvailable
    }

    private func notifySessionEnd(for error: Error) {
        guard let reason = nfcSessionEndReason(for: error) else {
            return
        }

        DispatchQueue.main.async {
            self.notifyListeners("nfcSessionEnd", data: ["reason": reason], retainUntilConsumed: true)
        }
    }

    private func pollingOptions(_ requestedPollingOptions: JSArray) -> NFCTagReaderSession.PollingOption {
        var pollingOptions: NFCTagReaderSession.PollingOption = []
        for option in requestedPollingOptions {
            guard let string = option as? String else {
                continue
            }

            if string == "iso14443" {
                pollingOptions.insert(.iso14443)
            } else if string == "iso15693" {
                pollingOptions.insert(.iso15693)
            } else if string == "iso18092" {
                pollingOptions.insert(.iso18092)
            } else if string == "pace" {
                if #available(iOS 16.0, *) {
                    pollingOptions.insert(.pace)
                }
            }
        }
        return pollingOptions
    }

    private func makeTagReaderSession(
        pollingOptions: NFCTagReaderSession.PollingOption,
        alertMessage: String?
    ) -> NFCTagReaderSession? {
        tagSessionActivated = false
        tagSessionPollingOptions = pollingOptions

        guard let session = NFCTagReaderSession(
            pollingOption: pollingOptions,
            delegate: self,
            queue: sessionQueue
        ) else {
            return nil
        }

        if let alertMessage, !alertMessage.isEmpty {
            session.alertMessage = alertMessage
        }

        tagReaderSession = session
        session.begin()
        return session
    }

    @objc public func startScanning(_ call: CAPPluginCall) {
        #if targetEnvironment(simulator)
        call.reject("NFC is not available on the simulator.", "NO_NFC")
        return
        #else
        let requestedSessionType = call.getString("iosSessionType", "ndef").lowercased()
        sessionType = requestedSessionType == "tag" ? "tag" : "ndef"

        guard isSessionAvailable(for: sessionType) else {
            let message = sessionType == "tag"
                ? "NFC tag reading is not available on this device. Ensure the TAG reader entitlement is enabled."
                : "NFC is not available on this device."
            call.reject(message, "NO_NFC")
            return
        }

        invalidateAfterFirstRead = call.getBool("invalidateAfterFirstRead", true)
        let alertMessage = call.getString("alertMessage")

        let requestedPollingOptions = call.getArray("iosPollingOptions", Self.defaultIosPollingOptions)
        let pollingOptions = self.pollingOptions(requestedPollingOptions)
        guard sessionType != "tag" || !pollingOptions.isEmpty else {
            call.reject("No valid polling options provided")
            return
        }

        DispatchQueue.main.async {
            // Invalidate any existing sessions
            self.ndefReaderSession?.invalidate()
            self.ndefReaderSession = nil
            self.tagReaderSession?.invalidate()
            self.tagReaderSession = nil
            self.currentTag = nil
            if let pendingStartCall = self.pendingStartCall, pendingStartCall !== call {
                pendingStartCall.reject("NFC scan was superseded by a new startScanning call.", "CANCELLED")
            }
            self.pendingStartCall = nil
            self.pendingStartSession = nil
            self.pendingAlertMessage = nil
            self.tagSessionTriedFallback = false

            if self.sessionType == "tag" {
                // Use NFCTagReaderSession for raw tag support
                self.pendingStartCall = call
                self.pendingAlertMessage = alertMessage

                let session = self.makeTagReaderSession(
                    pollingOptions: pollingOptions,
                    alertMessage: alertMessage
                )
                self.pendingStartSession = session

                guard session != nil else {
                    self.pendingStartCall = nil
                    self.pendingStartSession = nil
                    call.reject(
                        "Failed to create NFC tag reader session. Make sure the 'Near Field Communication Tag Reader Session Formats' entitlement includes the 'TAG' format in your app target.",
                        "NO_NFC"
                    )
                    return
                }

                return
            } else {
                // Use NFCNDEFReaderSession (default behavior)
                self.ndefReaderSession = NFCNDEFReaderSession(
                    delegate: self,
                    queue: self.sessionQueue,
                    invalidateAfterFirstRead: self.invalidateAfterFirstRead
                )
                if let alertMessage, !alertMessage.isEmpty {
                    self.ndefReaderSession?.alertMessage = alertMessage
                }
                self.ndefReaderSession?.begin()
            }

            call.resolve()
        }
        #endif
    }

    @objc public func stopScanning(_ call: CAPPluginCall) {
        DispatchQueue.main.async {
            self.ndefReaderSession?.invalidate()
            self.ndefReaderSession = nil
            self.tagReaderSession?.invalidate()
            self.tagReaderSession = nil
            self.currentTag = nil
        }
        call.resolve()
    }

    @objc public func write(_ call: CAPPluginCall) {
        guard currentTag != nil else {
            call.reject("No active NFC session or tag. Call startScanning and present a tag before writing.")
            return
        }

        guard let rawRecords = call.getArray("records") as? [[String: Any]] else {
            call.reject("records is required and must be an array.")
            return
        }

        do {
            let message = try buildMessage(from: rawRecords)
            performWriteToCurrentTag(message: message, call: call)
        } catch {
            call.reject("Invalid NDEF records payload.", nil, error)
        }
    }

    @objc public func erase(_ call: CAPPluginCall) {
        guard currentTag != nil else {
            call.reject("No active NFC session or tag. Call startScanning and present a tag before erasing.")
            return
        }

        let emptyRecord = NFCNDEFPayload(format: .empty, type: Data(), identifier: Data(), payload: Data())
        let message = NFCNDEFMessage(records: [emptyRecord])
        performWriteToCurrentTag(message: message, call: call)
    }

    private func performWriteToCurrentTag(message: NFCNDEFMessage, call: CAPPluginCall) {
        guard let tag = currentTag else {
            call.reject("No active NFC session or tag.")
            return
        }

        if let ndefSession = ndefReaderSession {
            // For NDEF session, we need to connect to the tag first
            performWrite(message: message, on: tag, session: ndefSession, call: call)
        } else if tagReaderSession != nil {
            // For Tag session, tag remains connected from discovery
            // Note: If the tag is removed and re-presented, the session will detect it as a new tag
            // and performWriteToTag may fail. Users should keep the tag in place after detection.
            performWriteToTag(message: message, on: tag, call: call)
        } else {
            call.reject("No active NFC session.")
        }
    }

    @objc public func makeReadOnly(_ call: CAPPluginCall) {
        call.reject("Making tags read only is not supported on iOS.", "UNSUPPORTED")
    }

    @objc public func share(_ call: CAPPluginCall) {
        call.reject("Peer-to-peer NFC sharing is not available on iOS.", "UNSUPPORTED")
    }

    @objc public func unshare(_ call: CAPPluginCall) {
        call.reject("Peer-to-peer NFC sharing is not available on iOS.", "UNSUPPORTED")
    }

    @objc public func getStatus(_ call: CAPPluginCall) {
        let status = isNfcAvailable() ? "NFC_OK" : "NO_NFC"
        call.resolve([
            "status": status
        ])
    }

    @objc public func showSettings(_ call: CAPPluginCall) {
        guard let url = URL(string: UIApplication.openSettingsURLString) else {
            call.reject("Unable to open application settings.")
            return
        }

        DispatchQueue.main.async {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
        call.resolve()
    }

    @objc public func getPluginVersion(_ call: CAPPluginCall) {
        call.resolve([
            "version": pluginVersion
        ])
    }

    @objc public func isSupported(_ call: CAPPluginCall) {
        #if targetEnvironment(simulator)
        call.resolve([
            "supported": false
        ])
        #else
        call.resolve([
            "supported": isNfcAvailable()
        ])
        #endif
    }

    private func performWrite(message: NFCNDEFMessage, on tag: NFCNDEFTag, session: NFCNDEFReaderSession, call: CAPPluginCall) {
        session.connect(to: tag) { [weak self] error in
            guard let self else {
                DispatchQueue.main.async { call.reject("Session is no longer available.") }
                return
            }

            if let error {
                DispatchQueue.main.async {
                    call.reject("Failed to connect to tag.", nil, error)
                }
                return
            }

            self.performWriteToTag(message: message, on: tag, call: call)
        }
    }

    private func performWriteToTag(message: NFCNDEFMessage, on tag: NFCNDEFTag, call: CAPPluginCall) {
        tag.queryNDEFStatus { status, capacity, statusError in
            if let statusError {
                DispatchQueue.main.async {
                    call.reject("Failed to query tag status.", nil, statusError)
                }
                return
            }

            switch status {
            case .readWrite:
                if capacity < message.length {
                    DispatchQueue.main.async {
                        call.reject("Tag capacity is insufficient for the provided message.")
                    }
                    return
                }
                tag.writeNDEF(message) { writeError in
                    DispatchQueue.main.async {
                        if let writeError {
                            call.reject("Failed to write NDEF message.", nil, writeError)
                        } else {
                            call.resolve()
                        }
                    }
                }
            case .readOnly:
                DispatchQueue.main.async {
                    call.reject("Tag is read only.")
                }
            case .notSupported:
                DispatchQueue.main.async {
                    call.reject("Tag does not support NDEF.")
                }
            @unknown default:
                DispatchQueue.main.async {
                    call.reject("Unknown tag status.")
                }
            }
        }
    }

    private func buildMessage(from records: [[String: Any]]) throws -> NFCNDEFMessage {
        if records.isEmpty {
            throw NfcPluginError.invalidPayload
        }

        let payloads = try records.map { record -> NFCNDEFPayload in
            guard let tnfValue = record["tnf"] as? NSNumber,
                  let typeArray = record["type"],
                  let idArray = record["id"],
                  let payloadArray = record["payload"] else {
                throw NfcPluginError.invalidPayload
            }

            let payload = NFCNDEFPayload(
                format: NFCTypeNameFormat(rawValue: UInt8(truncating: tnfValue)) ?? .unknown,
                type: data(from: typeArray),
                identifier: data(from: idArray),
                payload: data(from: payloadArray)
            )
            return payload
        }

        return NFCNDEFMessage(records: payloads)
    }

    private func data(from any: Any) -> Data {
        guard let numbers = any as? [NSNumber] else {
            return Data()
        }
        var bytes = [UInt8]()
        bytes.reserveCapacity(numbers.count)
        numbers.forEach { number in
            bytes.append(number.uint8Value)
        }
        return Data(bytes)
    }

    private func array(from data: Data?) -> [NSNumber]? {
        guard let data else {
            return nil
        }
        return data.map { NSNumber(value: $0) }
    }

    private func notify(event: [String: Any]) {
        DispatchQueue.main.async {
            self.notifyListeners("nfcEvent", data: event, retainUntilConsumed: true)
            guard let type = event["type"] as? String else {
                return
            }
            switch type {
            case "ndef":
                self.notifyListeners("ndefDiscovered", data: event, retainUntilConsumed: true)
            default:
                self.notifyListeners("tagDiscovered", data: event, retainUntilConsumed: true)
            }
        }
    }

    private func buildEvent(tag: NFCNDEFTag, status: NFCNDEFStatus, capacity: Int, message: NFCNDEFMessage?) -> [String: Any] {
        var tagInfo: [String: Any] = [:]
        if let identifierData = extractIdentifier(from: tag) {
            tagInfo["id"] = array(from: identifierData)
        }
        tagInfo["techTypes"] = detectTechTypes(for: tag)
        tagInfo["isWritable"] = status == .readWrite
        tagInfo["maxSize"] = capacity
        tagInfo["type"] = translateType(for: tag)

        if let message {
            tagInfo["ndefMessage"] = message.records.map { record in
                [
                    "tnf": NSNumber(value: record.typeNameFormat.rawValue),
                    "type": array(from: record.type),
                    "id": array(from: record.identifier),
                    "payload": array(from: record.payload)
                ].compactMapValues { $0 }
            }
        }

        return [
            "type": "ndef",
            "tag": tagInfo
        ]
    }

    private func extractIdentifier(from tag: NFCNDEFTag) -> Data? {
        if let miFare = tag as? NFCMiFareTag {
            return miFare.identifier
        }
        if let iso7816 = tag as? NFCISO7816Tag {
            return iso7816.identifier
        }
        if let iso15693 = tag as? NFCISO15693Tag {
            return iso15693.identifier
        }
        if let feliCa = tag as? NFCFeliCaTag {
            return Data(feliCa.currentIDm)
        }
        return nil
    }

    private func detectTechTypes(for tag: NFCNDEFTag) -> [String] {
        var types: [String] = []
        if tag is NFCMiFareTag {
            types.append("NFCMiFareTag")
        }
        if tag is NFCISO7816Tag {
            types.append("NFCISO7816Tag")
        }
        if tag is NFCISO15693Tag {
            types.append("NFCISO15693Tag")
        }
        if tag is NFCFeliCaTag {
            types.append("NFCFeliCaTag")
        }
        return types
    }

    private func translateType(for tag: NFCNDEFTag) -> String? {
        if let miFare = tag as? NFCMiFareTag {
            switch miFare.mifareFamily {
            case .plus:
                return "MIFARE Plus"
            case .ultralight:
                return "MIFARE Ultralight"
            case .desfire:
                return "MIFARE DESFire"
            case .unknown:
                return "MIFARE"
            @unknown default:
                return "MIFARE"
            }
        }
        if tag is NFCISO7816Tag {
            return "ISO 7816"
        }
        if tag is NFCISO15693Tag {
            return "ISO 15693"
        }
        if tag is NFCFeliCaTag {
            return "FeliCa"
        }
        return nil
    }
}

extension NfcPlugin: NFCNDEFReaderSessionDelegate {
    public func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
        currentTag = nil
        notifySessionEnd(for: error)
        if (error as NSError).code != NFCReaderError.readerSessionInvalidationErrorFirstNDEFTagRead.rawValue {
            DispatchQueue.main.async {
                let payload: [String: Any] = [
                    "status": self.isNfcAvailable() ? "NFC_OK" : "NO_NFC",
                    "enabled": self.isNfcAvailable()
                ]
                self.notifyListeners("nfcStateChange", data: payload, retainUntilConsumed: true)
            }
        }
        ndefReaderSession = nil
    }

    public func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
        guard let tag = tags.first else {
            return
        }

        session.connect(to: tag) { [weak self] error in
            guard let self else {
                return
            }

            if let error {
                session.invalidate(errorMessage: "Failed to connect to the tag: \(error.localizedDescription)")
                return
            }

            tag.queryNDEFStatus { status, capacity, statusError in
                if let statusError {
                    session.invalidate(errorMessage: "Failed to read tag status: \(statusError.localizedDescription)")
                    return
                }

                tag.readNDEF { message, readError in
                    if let readError {
                        if !self.invalidateAfterFirstRead && status == .readWrite {
                            self.currentTag = tag
                            let event = self.buildEvent(tag: tag, status: status, capacity: capacity, message: nil)
                            self.notify(event: event)
                            return
                        }
                        session.invalidate(errorMessage: "Failed to read NDEF message: \(readError.localizedDescription)")
                        return
                    }

                    self.currentTag = tag
                    let event = self.buildEvent(tag: tag, status: status, capacity: capacity, message: message)
                    self.notify(event: event)
                }
            }
        }
    }

    public func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
        guard !messages.isEmpty else {
            return
        }
        let event: [String: Any] = [
            "type": "ndef",
            "tag": [
                "ndefMessage": messages.first?.records.map { record in
                    [
                        "tnf": NSNumber(value: record.typeNameFormat.rawValue),
                        "type": array(from: record.type) ?? [],
                        "id": array(from: record.identifier) ?? [],
                        "payload": array(from: record.payload) ?? []
                    ]
                } ?? []
            ]
        ]
        notify(event: event)
    }
}

// MARK: - NFCTagReaderSessionDelegate
extension NfcPlugin: NFCTagReaderSessionDelegate {
    public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        guard session === tagReaderSession else {
            return
        }

        tagSessionActivated = true

        if session === pendingStartSession, let pendingCall = pendingStartCall {
            pendingStartCall = nil
            pendingStartSession = nil
            pendingAlertMessage = nil
            DispatchQueue.main.async {
                pendingCall.resolve()
            }
        }
    }

    public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
        let nfcError = error as NSError

        if let pendingCall = pendingStartCall {
            guard session === pendingStartSession else {
                return
            }

            currentTag = nil

            let canRetryWithoutFeliCa = !tagSessionActivated &&
                tagSessionPollingOptions.contains(.iso18092) &&
                !tagSessionTriedFallback &&
                (nfcError.code == NFCReaderError.readerErrorUnsupportedFeature.rawValue ||
                    nfcError.code == NFCReaderError.readerErrorSecurityViolation.rawValue)

            if canRetryWithoutFeliCa {
                let fallbackAlertMessage = pendingAlertMessage
                tagSessionTriedFallback = true
                tagReaderSession = nil
                pendingStartSession = nil

                DispatchQueue.main.async {
                    guard self.pendingStartCall === pendingCall, self.pendingStartSession == nil else {
                        return
                    }

                    let fallbackSession = self.makeTagReaderSession(
                        pollingOptions: [.iso14443, .iso15693],
                        alertMessage: fallbackAlertMessage
                    )
                    self.pendingStartSession = fallbackSession

                    if fallbackSession == nil {
                        self.pendingStartCall = nil
                        self.pendingStartSession = nil
                        self.pendingAlertMessage = nil
                        pendingCall.reject(
                            "Failed to start NFC tag session without FeliCa polling: \(error.localizedDescription)",
                            "NO_NFC",
                            error
                        )
                    }
                }
                return
            }

            notifySessionEnd(for: error)
            pendingStartCall = nil
            pendingStartSession = nil
            pendingAlertMessage = nil
            DispatchQueue.main.async {
                pendingCall.reject(
                    "Failed to start NFC tag session: \(error.localizedDescription)",
                    "NO_NFC",
                    error
                )
            }
            tagReaderSession = nil
            return
        }

        guard session === tagReaderSession else {
            return
        }

        currentTag = nil
        notifySessionEnd(for: error)

        // Don't emit state change for normal session completion (user canceled)
        // Also check for successful read completion
        if nfcError.code != NFCReaderError.readerSessionInvalidationErrorUserCanceled.rawValue {
            DispatchQueue.main.async {
                let payload: [String: Any] = [
                    "status": self.isNfcAvailable() ? "NFC_OK" : "NO_NFC",
                    "enabled": self.isNfcAvailable()
                ]
                self.notifyListeners("nfcStateChange", data: payload, retainUntilConsumed: true)
            }
        }
        tagReaderSession = nil
    }

    public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        // Handle multiple tags case - CoreNFC recommends invalidating with a message
        if tags.count > 1 {
            session.invalidate(errorMessage: "More than one tag detected. Please present only one tag.")
            return
        }

        guard let firstTag = tags.first else {
            return
        }

        session.connect(to: firstTag) { [weak self] error in
            guard let self else {
                return
            }

            if let error {
                session.invalidate(errorMessage: "Failed to connect to the tag: \(error.localizedDescription)")
                return
            }

            // Handle different tag types
            switch firstTag {
            case .miFare(let mifareTag):
                self.processTag(mifareTag, session: session)
            case .iso7816(let iso7816Tag):
                self.processTag(iso7816Tag, session: session)
            case .iso15693(let iso15693Tag):
                self.processTag(iso15693Tag, session: session)
            case .feliCa(let feliCaTag):
                self.processTag(feliCaTag, session: session)
            @unknown default:
                session.invalidate(errorMessage: "Unsupported tag type")
            }
        }
    }

    private func processTag(_ tag: NFCNDEFTag, session: NFCTagReaderSession) {
        // Try to read NDEF if available, otherwise emit tag with UID only
        tag.queryNDEFStatus { [weak self] status, capacity, error in
            guard let self else {
                return
            }

            if error == nil && status != .notSupported {
                // Tag supports NDEF, try to read it
                tag.readNDEF { [weak self] message, readError in
                    guard let self else {
                        return
                    }

                    // For blank/formatted tags that are writable but have no NDEF message yet,
                    // readNDEF may return nil message without an error, or return an error.
                    // We should emit the tag with its UID and writability info.
                    if message == nil {
                        // Blank tag or NDEF read failed - emit tag with UID and status info
                        self.emitTagEvent(tag: tag, status: status, capacity: capacity, message: nil, session: session)
                    } else if readError != nil && self.invalidateAfterFirstRead {
                        session.invalidate(errorMessage: "Failed to read NDEF message: \(readError!.localizedDescription)")
                    } else {
                        // Successfully read NDEF (or keep session open for writes after a read error)
                        self.currentTag = tag
                        let event = self.buildEvent(tag: tag, status: status, capacity: capacity, message: message)
                        self.notify(event: event)
                        if self.invalidateAfterFirstRead {
                            session.invalidate()
                        }
                    }
                }
            } else {
                // Tag doesn't support NDEF or query failed - just emit UID
                self.emitTagEvent(tag: tag, status: .notSupported, capacity: 0, message: nil, session: session)
            }
        }
    }

    private func emitTagEvent(tag: NFCNDEFTag, status: NFCNDEFStatus, capacity: Int, message: NFCNDEFMessage?, session: NFCTagReaderSession) {
        // Save the current tag for writing
        currentTag = tag

        var tagInfo: [String: Any] = [:]

        // Extract and add the tag ID (UID)
        if let identifierData = extractIdentifier(from: tag) {
            tagInfo["id"] = array(from: identifierData)
        }

        tagInfo["techTypes"] = detectTechTypes(for: tag)
        tagInfo["type"] = translateType(for: tag)

        // Include writability and capacity information
        if status != .notSupported {
            tagInfo["isWritable"] = status == .readWrite
            tagInfo["maxSize"] = capacity
        }

        if let message {
            tagInfo["ndefMessage"] = message.records.map { record in
                [
                    "tnf": NSNumber(value: record.typeNameFormat.rawValue),
                    "type": array(from: record.type),
                    "id": array(from: record.identifier),
                    "payload": array(from: record.payload)
                ].compactMapValues { $0 }
            }
        }

        let event: [String: Any] = [
            "type": message != nil ? "ndef" : "tag",
            "tag": tagInfo
        ]

        notify(event: event)

        if invalidateAfterFirstRead {
            session.invalidate()
        }
    }
}

enum NfcPluginError: Error {
    case invalidPayload
}
