import Photos

class WrapperAsset: Hashable, Equatable {
  var asset: Asset
  
  init(with asset: Asset) {
    self.asset = asset
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(self.asset.id)
  }
  
  static func == (lhs: WrapperAsset, rhs: WrapperAsset) -> Bool {
    return lhs.asset.id == rhs.asset.id
  }
}

class MediaManager {
  private let _defaults: UserDefaults
  private let _changeTokenKey = "immich:changeToken"
  
  init(with defaults: UserDefaults = .standard) {
    self._defaults = defaults
  }

  @available(iOS 16, *)
  func _getChangeToken() -> PHPersistentChangeToken? {
    guard let encodedToken = _defaults.data(forKey: _changeTokenKey) else {
        print("MediaManager::_getChangeToken: Change token not available in UserDefaults")
        return nil
    }

    do {
        return try NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: encodedToken)
    } catch {
        print("MediaManager::_getChangeToken: Cannot decode the token from UserDefaults")
        return nil
    }
  }
  
  @available(iOS 16, *)
  func _saveChangeToken(token: PHPersistentChangeToken) -> Void {
    do {
        let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
        _defaults.set(encodedToken, forKey: _changeTokenKey)
        print("MediaManager::_setChangeToken: Change token saved to UserDefaults")
    } catch {
        print("MediaManager::_setChangeToken: Failed to persist the token to UserDefaults: \(error)")
    }
  }
  
  @available(iOS 16, *)
  func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
    _saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
    completion(.success(()))
  }
  
  @available(iOS 16, *)
  func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
      // When we do not have access to photo library, return true to fallback to old sync
      completion(.success(true))
      return
    }
    
    guard let storedToken = _getChangeToken() else {
       // No token exists, perform the initial full sync
       print("MediaManager::shouldUseOldSync: No token found. Full sync required")
       completion(.success(true))
       return
    }

    do {
        _ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
      completion(.success(false))
    } catch {
      // fallback to using old sync when we cannot detect changes using the available token
      print("MediaManager::shouldUseOldSync: fetchPersistentChanges failed with error (\(error))")
      completion(.success(true))
    }
    
  }

  @available(iOS 16, *)
  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
      completion(.failure(PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)))
      return
    }
    
    guard let storedToken = _getChangeToken() else {
       // No token exists, definitely need a full sync
       print("MediaManager::getMediaChanges: No token found")
       completion(.failure(PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)))
       return
    }
    
    let currentToken = PHPhotoLibrary.shared().currentChangeToken
    if storedToken == currentToken {
      completion(.success(SyncDelta(hasChanges: false, updates: [], deletes: [])))
      return
    }

    do {
      let result = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
      let dateFormatter = ISO8601DateFormatter()
      dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

      var updatedArr: Set<WrapperAsset> = []
      var deletedArr: Set<String> = []

      for changes in result {
        let details = try changes.changeDetails(for: PHObjectType.asset)

        let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
        let deleted = details.deletedLocalIdentifiers
      
        let options = PHFetchOptions()
        options.includeHiddenAssets = false
        
        let updatedAssets = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)

        updatedAssets.enumerateObjects { (asset, _, _) in
   
          let id = asset.localIdentifier
          let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title()
          let type: Int64 = Int64(asset.mediaType.rawValue)
          let createdAt = asset.creationDate.map { dateFormatter.string(from: $0) }
          let updatedAt = asset.modificationDate.map { dateFormatter.string(from: $0) }
          let durationInSeconds: Int64 = Int64(asset.duration)
          
          let domainAsset = WrapperAsset(with: Asset(
              id: id,
              name: name,
              type: type,
              createdAt: createdAt,
              updatedAt: updatedAt,
              durationInSeconds: durationInSeconds,
              albumIds: self._getAlbumIds(forAsset: asset)
          ))
          
          updatedArr.insert(domainAsset)
        }
        
        deletedArr.formUnion(deleted)
      }

      let delta = SyncDelta(hasChanges: true, updates: Array(updatedArr.map { $0.asset }), deletes: Array(deletedArr))
    
      completion(.success(delta))
      return
    } catch {
      print("MediaManager::getMediaChanges: Error fetching persistent changes: \(error)")
      completion(.failure(PigeonError(code: "3", message: error.localizedDescription, details: nil)))
      return
    }
  }
  
  @available(iOS 16, *)
  func _getAlbumIds(forAsset: PHAsset) -> [String] {
    var albumIds: [String] = []
    let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]

    albumTypes.forEach { type in
      let collections = PHAssetCollection.fetchAssetCollectionsContaining(forAsset, with: type, options: nil)
      collections.enumerateObjects { (album, _, _) in
        albumIds.append(album.localIdentifier)
      }
    }
    return albumIds
  }

}