//
//  CloudKitRecordMapper.swift
//  Yattee
//
//  Maps SwiftData models to/from CloudKit CKRecords.
//

import CloudKit
import Foundation

/// Maps SwiftData models to CloudKit records and vice versa.
///
/// This MUST be `@MainActor`: the `toCKRecord(...)` methods read live SwiftData
/// `@Model` properties (e.g. `LocalPlaylistItem.authorName`) and the `to<Model>`
/// methods create/insert models. SwiftData models are bound to the main
/// `ModelContext` and are not thread-safe — when this was a standalone `actor`,
/// `await recordMapper.toCKRecord(...)` hopped off the main actor and read the
/// backing store from the wrong executor, causing EXC_BAD_ACCESS crashes.
@MainActor
final class CloudKitRecordMapper {
    private let zone: CKRecordZone

    /// Current schema version for CloudKit records.
    /// Increment this when record structure changes.
    static let currentSchemaVersion: Int64 = 2
    private var currentSchemaVersion: Int64 { Self.currentSchemaVersion }

    init(zone: CKRecordZone) {
        self.zone = zone
    }
    
    // MARK: - Subscription Mapping
    
    /// Converts a Subscription to a CKRecord.
    func toCKRecord(subscription: Subscription) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: subscription.sourceRawValue,
            globalProvider: subscription.providerName,
            instanceURLString: subscription.instanceURLString,
            externalExtractor: nil
        )
        let recordID = SyncableRecordType.subscription(channelID: subscription.channelID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.subscription, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Channel Identity
        record["channelID"] = subscription.channelID as CKRecordValue
        record["sourceRawValue"] = subscription.sourceRawValue as CKRecordValue
        record["instanceURLString"] = subscription.instanceURLString as CKRecordValue?

        // Channel Metadata
        record["name"] = subscription.name as CKRecordValue
        record["channelDescription"] = subscription.channelDescription as CKRecordValue?
        record["subscriberCount"] = subscription.subscriberCount.map { Int64($0) } as CKRecordValue?
        record["avatarURLString"] = subscription.avatarURLString as CKRecordValue?
        record["bannerURLString"] = subscription.bannerURLString as CKRecordValue?
        record["isVerified"] = (subscription.isVerified ? 1 : 0) as CKRecordValue

        // Subscription Metadata
        record["subscribedAt"] = subscription.subscribedAt as CKRecordValue
        record["lastUpdatedAt"] = subscription.lastUpdatedAt as CKRecordValue
        record["providerName"] = subscription.providerName as CKRecordValue?

        return record
    }
    
    /// Creates a Subscription from a CKRecord.
    func toSubscription(from record: CKRecord) throws -> Subscription {
        let recordType = RecordType.subscription
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let channelID = record["channelID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "channelID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let name = record["name"] as? String else {
            throw CloudKitError.missingRequiredField(field: "name", recordType: recordType)
        }
        guard let subscribedAt = record["subscribedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "subscribedAt", recordType: recordType)
        }
        guard let lastUpdatedAt = record["lastUpdatedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "lastUpdatedAt", recordType: recordType)
        }

        // Create subscription
        let subscription = Subscription(
            channelID: channelID,
            sourceRawValue: sourceRawValue,
            instanceURLString: record["instanceURLString"] as? String,
            name: name,
            channelDescription: record["channelDescription"] as? String,
            subscriberCount: (record["subscriberCount"] as? Int64).map(Int.init),
            avatarURLString: record["avatarURLString"] as? String,
            bannerURLString: record["bannerURLString"] as? String,
            isVerified: (record["isVerified"] as? Int64) == 1
        )

        // Set timestamps
        subscription.subscribedAt = subscribedAt
        subscription.lastUpdatedAt = lastUpdatedAt
        subscription.providerName = record["providerName"] as? String

        return subscription
    }
    
    // MARK: - WatchEntry Mapping
    
    /// Converts a WatchEntry to a CKRecord.
    func toCKRecord(watchEntry: WatchEntry) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: watchEntry.sourceRawValue,
            globalProvider: watchEntry.globalProvider,
            instanceURLString: watchEntry.instanceURLString,
            externalExtractor: watchEntry.externalExtractor
        )
        let recordID = SyncableRecordType.watchEntry(videoID: watchEntry.videoID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.watchEntry, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Video Identity
        record["videoID"] = watchEntry.videoID as CKRecordValue
        record["sourceRawValue"] = watchEntry.sourceRawValue as CKRecordValue
        record["globalProvider"] = watchEntry.globalProvider as CKRecordValue?
        record["instanceURLString"] = watchEntry.instanceURLString as CKRecordValue?
        record["peertubeUUID"] = watchEntry.peertubeUUID as CKRecordValue?
        record["externalExtractor"] = watchEntry.externalExtractor as CKRecordValue?
        record["externalURLString"] = watchEntry.externalURLString as CKRecordValue?

        // Video Metadata (cached)
        record["title"] = watchEntry.title as CKRecordValue
        record["authorName"] = watchEntry.authorName as CKRecordValue
        record["authorID"] = watchEntry.authorID as CKRecordValue
        record["duration"] = watchEntry.duration as CKRecordValue
        record["thumbnailURLString"] = watchEntry.thumbnailURLString as CKRecordValue?

        // Watch Progress
        record["watchedSeconds"] = watchEntry.watchedSeconds as CKRecordValue
        record["isFinished"] = (watchEntry.isFinished ? 1 : 0) as CKRecordValue
        record["finishedAt"] = watchEntry.finishedAt as CKRecordValue?
        record["isLive"] = (watchEntry.isLive ? 1 : 0) as CKRecordValue

        // Timestamps
        record["createdAt"] = watchEntry.createdAt as CKRecordValue
        record["updatedAt"] = watchEntry.updatedAt as CKRecordValue

        return record
    }
    
    /// Creates a WatchEntry from a CKRecord.
    func toWatchEntry(from record: CKRecord) throws -> WatchEntry {
        let recordType = RecordType.watchEntry
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let videoID = record["videoID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "videoID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let title = record["title"] as? String else {
            throw CloudKitError.missingRequiredField(field: "title", recordType: recordType)
        }
        guard let authorName = record["authorName"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorName", recordType: recordType)
        }
        guard let authorID = record["authorID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorID", recordType: recordType)
        }
        guard let duration = record["duration"] as? Double else {
            throw CloudKitError.missingRequiredField(field: "duration", recordType: recordType)
        }
        guard let watchedSeconds = record["watchedSeconds"] as? Double else {
            throw CloudKitError.missingRequiredField(field: "watchedSeconds", recordType: recordType)
        }
        guard let isFinishedInt = record["isFinished"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "isFinished", recordType: recordType)
        }
        guard let createdAt = record["createdAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "createdAt", recordType: recordType)
        }
        guard let updatedAt = record["updatedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "updatedAt", recordType: recordType)
        }

        // Create watch entry
        let watchEntry = WatchEntry(
            videoID: videoID,
            sourceRawValue: sourceRawValue,
            globalProvider: record["globalProvider"] as? String,
            instanceURLString: record["instanceURLString"] as? String,
            peertubeUUID: record["peertubeUUID"] as? String,
            externalExtractor: record["externalExtractor"] as? String,
            externalURLString: record["externalURLString"] as? String,
            title: title,
            authorName: authorName,
            authorID: authorID,
            duration: duration,
            thumbnailURLString: record["thumbnailURLString"] as? String,
            watchedSeconds: watchedSeconds,
            isFinished: isFinishedInt == 1
        )

        // Set timestamps
        watchEntry.createdAt = createdAt
        watchEntry.updatedAt = updatedAt
        watchEntry.finishedAt = record["finishedAt"] as? Date

        // Optional field - records written before this field existed default to false,
        // so it is read leniently rather than bumping the schema version.
        watchEntry.isLive = (record["isLive"] as? Int64) == 1

        return watchEntry
    }
    
    // MARK: - Bookmark Mapping
    
    /// Converts a Bookmark to a CKRecord.
    func toCKRecord(bookmark: Bookmark) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: bookmark.sourceRawValue,
            globalProvider: bookmark.globalProvider,
            instanceURLString: bookmark.instanceURLString,
            externalExtractor: bookmark.externalExtractor
        )
        let recordID = SyncableRecordType.bookmark(videoID: bookmark.videoID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.bookmark, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Video Identity
        record["videoID"] = bookmark.videoID as CKRecordValue
        record["sourceRawValue"] = bookmark.sourceRawValue as CKRecordValue
        record["globalProvider"] = bookmark.globalProvider as CKRecordValue?
        record["instanceURLString"] = bookmark.instanceURLString as CKRecordValue?
        record["peertubeUUID"] = bookmark.peertubeUUID as CKRecordValue?
        record["externalExtractor"] = bookmark.externalExtractor as CKRecordValue?
        record["externalURLString"] = bookmark.externalURLString as CKRecordValue?

        // Video Metadata (cached)
        record["title"] = bookmark.title as CKRecordValue
        record["authorName"] = bookmark.authorName as CKRecordValue
        record["authorID"] = bookmark.authorID as CKRecordValue
        record["duration"] = bookmark.duration as CKRecordValue
        record["thumbnailURLString"] = bookmark.thumbnailURLString as CKRecordValue?
        record["isLive"] = (bookmark.isLive ? 1 : 0) as CKRecordValue
        record["viewCount"] = bookmark.viewCount.map { Int64($0) } as CKRecordValue?
        record["publishedAt"] = bookmark.publishedAt as CKRecordValue?
        record["publishedText"] = bookmark.publishedText as CKRecordValue?

        // Bookmark Metadata
        record["createdAt"] = bookmark.createdAt as CKRecordValue
        record["note"] = bookmark.note as CKRecordValue?
        record["noteModifiedAt"] = bookmark.noteModifiedAt as CKRecordValue?
        record["tags"] = bookmark.tags as CKRecordValue
        record["tagsModifiedAt"] = bookmark.tagsModifiedAt as CKRecordValue?
        record["sortOrder"] = Int64(bookmark.sortOrder) as CKRecordValue

        return record
    }
    
    /// Creates a Bookmark from a CKRecord.
    func toBookmark(from record: CKRecord) throws -> Bookmark {
        let recordType = RecordType.bookmark
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let videoID = record["videoID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "videoID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let title = record["title"] as? String else {
            throw CloudKitError.missingRequiredField(field: "title", recordType: recordType)
        }
        guard let authorName = record["authorName"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorName", recordType: recordType)
        }
        guard let authorID = record["authorID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorID", recordType: recordType)
        }
        guard let duration = record["duration"] as? Double else {
            throw CloudKitError.missingRequiredField(field: "duration", recordType: recordType)
        }
        guard let isLiveInt = record["isLive"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "isLive", recordType: recordType)
        }
        guard let createdAt = record["createdAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "createdAt", recordType: recordType)
        }
        guard let sortOrderInt = record["sortOrder"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "sortOrder", recordType: recordType)
        }

        // Create bookmark
        let bookmark = Bookmark(
            videoID: videoID,
            sourceRawValue: sourceRawValue,
            globalProvider: record["globalProvider"] as? String,
            instanceURLString: record["instanceURLString"] as? String,
            peertubeUUID: record["peertubeUUID"] as? String,
            externalExtractor: record["externalExtractor"] as? String,
            externalURLString: record["externalURLString"] as? String,
            title: title,
            authorName: authorName,
            authorID: authorID,
            duration: duration,
            thumbnailURLString: record["thumbnailURLString"] as? String,
            isLive: isLiveInt == 1,
            viewCount: (record["viewCount"] as? Int64).map(Int.init),
            publishedAt: record["publishedAt"] as? Date,
            publishedText: record["publishedText"] as? String,
            note: record["note"] as? String,
            noteModifiedAt: record["noteModifiedAt"] as? Date,
            tags: record["tags"] as? [String] ?? [],
            tagsModifiedAt: record["tagsModifiedAt"] as? Date,
            sortOrder: Int(sortOrderInt)
        )

        // Set timestamp
        bookmark.createdAt = createdAt

        return bookmark
    }
    
    // MARK: - LocalPlaylist Mapping
    
    /// Converts a LocalPlaylist to a CKRecord.
    func toCKRecord(playlist: LocalPlaylist) -> CKRecord {
        let recordID = SyncableRecordType.localPlaylist(id: playlist.id).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.localPlaylist, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Playlist Metadata
        record["playlistID"] = playlist.id.uuidString as CKRecordValue
        record["title"] = playlist.title as CKRecordValue
        record["playlistDescription"] = playlist.playlistDescription as CKRecordValue?
        record["createdAt"] = playlist.createdAt as CKRecordValue
        record["updatedAt"] = playlist.updatedAt as CKRecordValue

        return record
    }
    
    /// Creates a LocalPlaylist from a CKRecord.
    func toLocalPlaylist(from record: CKRecord) throws -> LocalPlaylist {
        let recordType = RecordType.localPlaylist
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let playlistIDString = record["playlistID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "playlistID", recordType: recordType)
        }
        guard let playlistID = UUID(uuidString: playlistIDString) else {
            throw CloudKitError.typeMismatch(field: "playlistID", recordType: recordType, expected: "valid UUID string")
        }
        guard let title = record["title"] as? String else {
            throw CloudKitError.missingRequiredField(field: "title", recordType: recordType)
        }
        guard let createdAt = record["createdAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "createdAt", recordType: recordType)
        }
        guard let updatedAt = record["updatedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "updatedAt", recordType: recordType)
        }

        // Create playlist
        let playlist = LocalPlaylist(
            id: playlistID,
            title: title,
            description: record["playlistDescription"] as? String
        )

        // Set timestamps
        playlist.createdAt = createdAt
        playlist.updatedAt = updatedAt

        return playlist
    }
    
    // MARK: - LocalPlaylistItem Mapping
    
    /// Converts a LocalPlaylistItem to a CKRecord.
    func toCKRecord(playlistItem: LocalPlaylistItem) -> CKRecord {
        let recordID = SyncableRecordType.localPlaylistItem(id: playlistItem.id).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.localPlaylistItem, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Item Identity
        record["itemID"] = playlistItem.id.uuidString as CKRecordValue
        record["playlistID"] = playlistItem.playlist?.id.uuidString as CKRecordValue?
        record["sortOrder"] = Int64(playlistItem.sortOrder) as CKRecordValue

        // Video Identity
        record["videoID"] = playlistItem.videoID as CKRecordValue
        record["sourceRawValue"] = playlistItem.sourceRawValue as CKRecordValue
        record["globalProvider"] = playlistItem.globalProvider as CKRecordValue?
        record["instanceURLString"] = playlistItem.instanceURLString as CKRecordValue?
        record["peertubeUUID"] = playlistItem.peertubeUUID as CKRecordValue?
        record["externalExtractor"] = playlistItem.externalExtractor as CKRecordValue?
        record["externalURLString"] = playlistItem.externalURLString as CKRecordValue?

        // Video Metadata
        record["title"] = playlistItem.title as CKRecordValue
        record["authorName"] = playlistItem.authorName as CKRecordValue
        record["authorID"] = playlistItem.authorID as CKRecordValue
        record["duration"] = playlistItem.duration as CKRecordValue
        record["thumbnailURLString"] = playlistItem.thumbnailURLString as CKRecordValue?
        record["isLive"] = (playlistItem.isLive ? 1 : 0) as CKRecordValue
        record["addedAt"] = playlistItem.addedAt as CKRecordValue

        return record
    }
    
    /// Creates a LocalPlaylistItem from a CKRecord.
    func toLocalPlaylistItem(from record: CKRecord) throws -> (item: LocalPlaylistItem, playlistID: UUID?) {
        let recordType = RecordType.localPlaylistItem
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let itemIDString = record["itemID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "itemID", recordType: recordType)
        }
        guard let itemID = UUID(uuidString: itemIDString) else {
            throw CloudKitError.typeMismatch(field: "itemID", recordType: recordType, expected: "valid UUID string")
        }
        guard let sortOrderInt = record["sortOrder"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "sortOrder", recordType: recordType)
        }
        guard let videoID = record["videoID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "videoID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let title = record["title"] as? String else {
            throw CloudKitError.missingRequiredField(field: "title", recordType: recordType)
        }
        guard let authorName = record["authorName"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorName", recordType: recordType)
        }
        guard let authorID = record["authorID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorID", recordType: recordType)
        }
        guard let duration = record["duration"] as? Double else {
            throw CloudKitError.missingRequiredField(field: "duration", recordType: recordType)
        }
        guard let isLiveInt = record["isLive"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "isLive", recordType: recordType)
        }
        guard let addedAt = record["addedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "addedAt", recordType: recordType)
        }

        // Extract playlist ID
        let playlistID: UUID?
        if let playlistIDString = record["playlistID"] as? String {
            playlistID = UUID(uuidString: playlistIDString)
        } else {
            playlistID = nil
        }

        // Create playlist item
        let item = LocalPlaylistItem(
            id: itemID,
            sortOrder: Int(sortOrderInt),
            videoID: videoID,
            sourceRawValue: sourceRawValue,
            globalProvider: record["globalProvider"] as? String,
            instanceURLString: record["instanceURLString"] as? String,
            peertubeUUID: record["peertubeUUID"] as? String,
            externalExtractor: record["externalExtractor"] as? String,
            externalURLString: record["externalURLString"] as? String,
            title: title,
            authorName: authorName,
            authorID: authorID,
            duration: duration,
            thumbnailURLString: record["thumbnailURLString"] as? String,
            isLive: isLiveInt == 1
        )

        // Set timestamp
        item.addedAt = addedAt

        return (item, playlistID)
    }
    
    // MARK: - SearchHistory Mapping
    
    /// Converts a SearchHistory to a CKRecord.
    func toCKRecord(searchHistory: SearchHistory) -> CKRecord {
        let recordID = SyncableRecordType.searchHistory(id: searchHistory.id).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.searchHistory, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        record["searchID"] = searchHistory.id.uuidString as CKRecordValue
        record["query"] = searchHistory.query as CKRecordValue
        record["searchedAt"] = searchHistory.searchedAt as CKRecordValue

        return record
    }
    
    /// Creates a SearchHistory from a CKRecord.
    func toSearchHistory(from record: CKRecord) throws -> SearchHistory {
        let recordType = RecordType.searchHistory
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let searchIDString = record["searchID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "searchID", recordType: recordType)
        }
        guard let searchID = UUID(uuidString: searchIDString) else {
            throw CloudKitError.typeMismatch(field: "searchID", recordType: recordType, expected: "valid UUID string")
        }
        guard let query = record["query"] as? String else {
            throw CloudKitError.missingRequiredField(field: "query", recordType: recordType)
        }
        guard let searchedAt = record["searchedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "searchedAt", recordType: recordType)
        }

        return SearchHistory(id: searchID, query: query, searchedAt: searchedAt)
    }
    
    // MARK: - RecentChannel Mapping
    
    /// Converts a RecentChannel to a CKRecord.
    func toCKRecord(recentChannel: RecentChannel) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: recentChannel.sourceRawValue,
            globalProvider: nil,
            instanceURLString: recentChannel.instanceURLString,
            externalExtractor: nil
        )
        let recordID = SyncableRecordType.recentChannel(channelID: recentChannel.channelID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.recentChannel, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        record["recentID"] = recentChannel.id.uuidString as CKRecordValue
        record["channelID"] = recentChannel.channelID as CKRecordValue
        record["sourceRawValue"] = recentChannel.sourceRawValue as CKRecordValue
        record["instanceURLString"] = recentChannel.instanceURLString as CKRecordValue?
        record["name"] = recentChannel.name as CKRecordValue
        record["thumbnailURLString"] = recentChannel.thumbnailURLString as CKRecordValue?
        record["subscriberCount"] = recentChannel.subscriberCount.map { Int64($0) } as CKRecordValue?
        record["isVerified"] = (recentChannel.isVerified ? 1 : 0) as CKRecordValue
        record["visitedAt"] = recentChannel.visitedAt as CKRecordValue

        return record
    }
    
    /// Creates a RecentChannel from a CKRecord.
    func toRecentChannel(from record: CKRecord) throws -> RecentChannel {
        let recordType = RecordType.recentChannel
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let recentIDString = record["recentID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "recentID", recordType: recordType)
        }
        guard let recentID = UUID(uuidString: recentIDString) else {
            throw CloudKitError.typeMismatch(field: "recentID", recordType: recordType, expected: "valid UUID string")
        }
        guard let channelID = record["channelID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "channelID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let name = record["name"] as? String else {
            throw CloudKitError.missingRequiredField(field: "name", recordType: recordType)
        }
        guard let isVerifiedInt = record["isVerified"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "isVerified", recordType: recordType)
        }
        guard let visitedAt = record["visitedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "visitedAt", recordType: recordType)
        }

        return RecentChannel(
            id: recentID,
            channelID: channelID,
            sourceRawValue: sourceRawValue,
            instanceURLString: record["instanceURLString"] as? String,
            name: name,
            thumbnailURLString: record["thumbnailURLString"] as? String,
            subscriberCount: (record["subscriberCount"] as? Int64).map(Int.init),
            isVerified: isVerifiedInt == 1,
            visitedAt: visitedAt
        )
    }
    
    // MARK: - RecentPlaylist Mapping
    
    /// Converts a RecentPlaylist to a CKRecord.
    func toCKRecord(recentPlaylist: RecentPlaylist) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: recentPlaylist.sourceRawValue,
            globalProvider: nil,
            instanceURLString: recentPlaylist.instanceURLString,
            externalExtractor: nil
        )
        let recordID = SyncableRecordType.recentPlaylist(playlistID: recentPlaylist.playlistID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.recentPlaylist, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        record["recentID"] = recentPlaylist.id.uuidString as CKRecordValue
        record["playlistID"] = recentPlaylist.playlistID as CKRecordValue
        record["sourceRawValue"] = recentPlaylist.sourceRawValue as CKRecordValue
        record["instanceURLString"] = recentPlaylist.instanceURLString as CKRecordValue?
        record["title"] = recentPlaylist.title as CKRecordValue
        record["authorName"] = recentPlaylist.authorName as CKRecordValue
        record["videoCount"] = Int64(recentPlaylist.videoCount) as CKRecordValue
        record["thumbnailURLString"] = recentPlaylist.thumbnailURLString as CKRecordValue?
        record["visitedAt"] = recentPlaylist.visitedAt as CKRecordValue

        return record
    }
    
    /// Creates a RecentPlaylist from a CKRecord.
    func toRecentPlaylist(from record: CKRecord) throws -> RecentPlaylist {
        let recordType = RecordType.recentPlaylist
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let recentIDString = record["recentID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "recentID", recordType: recordType)
        }
        guard let recentID = UUID(uuidString: recentIDString) else {
            throw CloudKitError.typeMismatch(field: "recentID", recordType: recordType, expected: "valid UUID string")
        }
        guard let playlistID = record["playlistID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "playlistID", recordType: recordType)
        }
        guard let sourceRawValue = record["sourceRawValue"] as? String else {
            throw CloudKitError.missingRequiredField(field: "sourceRawValue", recordType: recordType)
        }
        guard let title = record["title"] as? String else {
            throw CloudKitError.missingRequiredField(field: "title", recordType: recordType)
        }
        guard let authorName = record["authorName"] as? String else {
            throw CloudKitError.missingRequiredField(field: "authorName", recordType: recordType)
        }
        guard let videoCountInt = record["videoCount"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "videoCount", recordType: recordType)
        }
        guard let visitedAt = record["visitedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "visitedAt", recordType: recordType)
        }

        return RecentPlaylist(
            id: recentID,
            playlistID: playlistID,
            sourceRawValue: sourceRawValue,
            instanceURLString: record["instanceURLString"] as? String,
            title: title,
            authorName: authorName,
            videoCount: Int(videoCountInt),
            thumbnailURLString: record["thumbnailURLString"] as? String,
            visitedAt: visitedAt
        )
    }
    
    // MARK: - ChannelNotificationSettings Mapping
    
    /// Converts a ChannelNotificationSettings to a CKRecord.
    func toCKRecord(channelNotificationSettings settings: ChannelNotificationSettings) -> CKRecord {
        let scope = SourceScope.from(
            sourceRawValue: settings.sourceRawValue,
            globalProvider: settings.globalProvider,
            instanceURLString: settings.instanceURLString,
            externalExtractor: nil
        )
        let recordID = SyncableRecordType.channelNotificationSettings(channelID: settings.channelID, scope: scope).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.channelNotificationSettings, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        record["channelID"] = settings.channelID as CKRecordValue
        record["notificationsEnabled"] = (settings.notificationsEnabled ? 1 : 0) as CKRecordValue
        record["updatedAt"] = settings.updatedAt as CKRecordValue
        record["sourceRawValue"] = settings.sourceRawValue as CKRecordValue
        record["instanceURLString"] = settings.instanceURLString as CKRecordValue?
        record["globalProvider"] = settings.globalProvider as CKRecordValue?

        return record
    }
    
    /// Creates a ChannelNotificationSettings from a CKRecord.
    func toChannelNotificationSettings(from record: CKRecord) throws -> ChannelNotificationSettings {
        let recordType = RecordType.channelNotificationSettings
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let channelID = record["channelID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "channelID", recordType: recordType)
        }
        guard let notificationsEnabledInt = record["notificationsEnabled"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "notificationsEnabled", recordType: recordType)
        }
        guard let updatedAt = record["updatedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "updatedAt", recordType: recordType)
        }

        let settings = ChannelNotificationSettings(
            channelID: channelID,
            notificationsEnabled: notificationsEnabledInt == 1,
            sourceRawValue: (record["sourceRawValue"] as? String) ?? "global",
            instanceURLString: record["instanceURLString"] as? String,
            globalProvider: record["globalProvider"] as? String
        )
        settings.updatedAt = updatedAt

        return settings
    }
    
    // MARK: - LayoutPreset Mapping
    
    /// Converts a LayoutPreset to a CKRecord.
    func toCKRecord(preset: LayoutPreset) throws -> CKRecord {
        let recordID = SyncableRecordType.controlsPreset(id: preset.id).recordID(in: zone)
        let record = CKRecord(recordType: RecordType.controlsPreset, recordID: recordID)

        // Schema version
        record["schemaVersion"] = currentSchemaVersion as CKRecordValue

        // Preset metadata
        record["presetID"] = preset.id.uuidString as CKRecordValue
        record["name"] = preset.name as CKRecordValue
        record["createdAt"] = preset.createdAt as CKRecordValue
        record["updatedAt"] = preset.updatedAt as CKRecordValue
        record["isBuiltIn"] = (preset.isBuiltIn ? 1 : 0) as CKRecordValue
        record["deviceClass"] = preset.deviceClass.rawValue as CKRecordValue

        // Encode layout as JSON string
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let layoutData = try encoder.encode(preset.layout)
        let layoutJSON = String(data: layoutData, encoding: .utf8) ?? "{}"
        record["layoutJSON"] = layoutJSON as CKRecordValue

        return record
    }
    
    /// Creates a LayoutPreset from a CKRecord.
    func toLayoutPreset(from record: CKRecord) throws -> LayoutPreset {
        let recordType = RecordType.controlsPreset
        guard record.recordType == recordType else {
            throw CloudKitError.recordNotFound
        }

        // Version check - treat missing version as v1 (first versioned release)
        let version = (record["schemaVersion"] as? Int64) ?? 1
        guard version <= currentSchemaVersion else {
            throw CloudKitError.unsupportedSchemaVersion(version: version, recordType: recordType)
        }

        // Required fields with specific error messages
        guard let presetIDString = record["presetID"] as? String else {
            throw CloudKitError.missingRequiredField(field: "presetID", recordType: recordType)
        }
        guard let presetID = UUID(uuidString: presetIDString) else {
            throw CloudKitError.typeMismatch(field: "presetID", recordType: recordType, expected: "valid UUID string")
        }
        guard let name = record["name"] as? String else {
            throw CloudKitError.missingRequiredField(field: "name", recordType: recordType)
        }
        guard let createdAt = record["createdAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "createdAt", recordType: recordType)
        }
        guard let updatedAt = record["updatedAt"] as? Date else {
            throw CloudKitError.missingRequiredField(field: "updatedAt", recordType: recordType)
        }
        guard let isBuiltInInt = record["isBuiltIn"] as? Int64 else {
            throw CloudKitError.missingRequiredField(field: "isBuiltIn", recordType: recordType)
        }
        guard let deviceClassRaw = record["deviceClass"] as? String else {
            throw CloudKitError.missingRequiredField(field: "deviceClass", recordType: recordType)
        }
        guard let deviceClass = DeviceClass(rawValue: deviceClassRaw) else {
            throw CloudKitError.typeMismatch(field: "deviceClass", recordType: recordType, expected: "valid DeviceClass value")
        }
        guard let layoutJSON = record["layoutJSON"] as? String else {
            throw CloudKitError.missingRequiredField(field: "layoutJSON", recordType: recordType)
        }
        guard let layoutData = layoutJSON.data(using: .utf8) else {
            throw CloudKitError.typeMismatch(field: "layoutJSON", recordType: recordType, expected: "valid UTF-8 JSON string")
        }

        // Decode layout
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let layout = try decoder.decode(PlayerControlsLayout.self, from: layoutData)

        return LayoutPreset(
            id: presetID,
            name: name,
            createdAt: createdAt,
            updatedAt: updatedAt,
            isBuiltIn: isBuiltInInt == 1,
            deviceClass: deviceClass,
            layout: layout
        )
    }
}
