import 'package:mobdr/db/box_user.dart';
import 'package:mobdr/db/box_log.dart';
import 'package:mobdr/db/box_etab.dart';
import 'package:mobdr/db/box_photo_competitor.dart';
import 'package:mobdr/db/box_visit.dart';
import 'package:mobdr/db/box_visit_tag.dart';
import 'package:mobdr/db/box_visit_photo.dart';
import 'package:mobdr/db/box_photo_typology.dart';

import 'model.dart';
import 'objectbox.g.dart'; // created by `flutter pub run build_runner build`

/// Provides access to the ObjectBox Store throughout the app.
///
/// Create this in the apps main function.
class ObjectBox {
  /// The Store of this app.
  late final Store store;

  /// A Box of notes.
  late final Box<Note> noteBox;

  /// A Box of user
  late final Box<User> userBox;

  /// A Box of etablishment
  late final Box<Etab> etabBox;

  /// A Box of etablishment competitor
  late final Box<EtabCompetitor> etabCompetitorBox;

  /// A Box of visit
  late final Box<Visit> visitBox;

  /// A Box for all visit tag by distributor / language
  late final Box<VisitTag> visitTagBox;

  /// A Box of visit Photo
  late final Box<VisitPhoto> visitPhotoBox;

  /// A Box of photo typology
  late final Box<PhotoTypology> PhotoTypologyBox;

  /// A Box of log.
  late final Box<Log> logBox;

  ObjectBox._create(this.store) {
    noteBox = Box<Note>(store);
    userBox = Box<User>(store);
    etabBox = Box<Etab>(store);
    etabCompetitorBox = Box<EtabCompetitor>(store);
    visitBox = Box<Visit>(store);
    visitTagBox = Box<VisitTag>(store);
    PhotoTypologyBox = Box<PhotoTypology>(store);
    visitPhotoBox = Box<VisitPhoto>(store);
    logBox = Box<Log>(store);
  }

  /// Create an instance of ObjectBox to use throughout the app.
  static Future<ObjectBox> create() async {
    // Future<Store> openStore() {...} is defined in the generated objectbox.g.dart
    final store = await openStore();
    return ObjectBox._create(store);
  }

  /**
   * TODO: A SUPPRIMER + Box note
   */
  Stream<List<Note>> getNotes() {
    // Query for all notes, sorted by their date.
    // https://docs.objectbox.io/queries
    final builder = noteBox.query().order(Note_.date, flags: Order.descending);
    // Build and watch the query,
    // set triggerImmediately to emit the query immediately on listen.
    return builder
        .watch(triggerImmediately: true)
        // Map it to a list of notes to be used by a StreamBuilder.
        .map((query) => query.find());
  }

  /// Add a note within a transaction.
  ///
  /// To avoid frame drops, run ObjectBox operations that take longer than a
  /// few milliseconds, e.g. putting many objects, in an isolate with its
  /// own Store instance.
  /// For this example only a single object is put which would also be fine if
  /// done here directly.
  Future<void> addNote(String text) =>
      store.runInTransactionAsync(TxMode.write, _addNoteInTx, text);

  /// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983)
  /// not using a closure as it may capture more objects than expected.
  /// These might not be send-able to an isolate. See Store.runAsync for details.
  static void _addNoteInTx(Store store, String text) {
    // Perform ObjectBox operations that take longer than a few milliseconds
    // here. To keep it simple, this example just puts a single object.
    store.box<Note>().put(Note(text));
  }

  /// USER ---------------------------------------------------------------------
  ///

  void addUSer(int _id_utilisateur, String _login, String _nom, String _prenom,
      String _photo) {
    userBox.put(User(
        id_utilisateur: _id_utilisateur,
        login: _login,
        nom: _nom,
        prenom: _prenom,
        photo: _photo));
  }

  String getUserAvatar(int id_utilisateur) {
    final query =
        userBox.query(User_.id_utilisateur.equals(id_utilisateur)).build();
    var p = query.findFirst();
    return p!.photo;
  }

  /// ETAB ---------------------------------------------------------------------
  ///

  // A function that converts a response body into a List<Etab>.
  List<Etab> parseEtabs(List responseDataList) {
    final parsed = responseDataList.cast<Map<String, dynamic>>();
    return parsed.map<Etab>((json) => Etab.fromJson(json)).toList();
  }

  Future<void> addEtabs(List<dynamic> _listEtabs) =>
      store.runInTransactionAsync(
          TxMode.write, _addEtabsInTx, parseEtabs(_listEtabs));

  static void _addEtabsInTx(Store store, _Etabs) {
    store.box<Etab>().putMany(_Etabs);
  }

  Future<void> addEtab(int _id_etab, String _nom, String _mail, String _tel,
          String _photo_principale, String _longitude, String _latitude) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addEtabInTx,
          Etab(
              id_etab: _id_etab,
              nom: _nom,
              mail: _mail,
              tel: _tel,
              url_photo_principale: _photo_principale,
              longitude: _longitude,
              latitude: _latitude));

  static void _addEtabInTx(Store store, _Etab) {
    store.box<Etab>().put(_Etab);
  }

  String getEtabPhotoPrincipale(int _id_etab) {
    final query = etabBox.query(Etab_.id_etab.equals(_id_etab)).build();
    var p = query.findFirst();
    return p!.url_photo_principale;
  }

  int getEtabCount() {
    return etabBox.count();
  }

  /// ETAB COMPETITOR ---------------------------------------------------------------
  ///

  // A function that converts a response body list into a List<Competitor>.
  List<EtabCompetitor> parseEtabCompetitor(List responseDataList) {
    final parsed = responseDataList.cast<Map<String, dynamic>>();
    return parsed
        .map<EtabCompetitor>((json) => EtabCompetitor.fromJson(json))
        .toList();
  }

  Future<void> addEtabCompetitors(List<dynamic> _listPhotoCompetitors) =>
      store.runInTransactionAsync(TxMode.write, _addEtabCompetitorsInTx,
          parseEtabCompetitor(_listPhotoCompetitors));

  static void _addEtabCompetitorsInTx(Store store, _Competitors) {
    store.box<EtabCompetitor>().putMany(_Competitors);
  }

  Future<void> addEtabCompetitor(
          int _id_concurrence_lien, int _id_etab, _nom) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addEtabCompetitorInTx,
          EtabCompetitor(
            id_concurrence_lien: _id_concurrence_lien,
            id_etab: _id_etab,
            nom: _nom,
          ));

  static void _addEtabCompetitorInTx(Store store, _Competitor) {
    store.box<EtabCompetitor>().put(_Competitor);
  }

  List<EtabCompetitor> getEtabCompetitorList(int _id_etab) {
    final query = etabCompetitorBox
        .query(EtabCompetitor_.id_etab.equals(_id_etab))
        .order(EtabCompetitor_.nom)
        .build();
    final etabCompetitors = query.find();

    return etabCompetitors.toList();
  }

  /// Retrieves a etab competitor object from the ObjectBox database with the specified ID.
  ///
  /// Parameters:
  ///   _id_concurrence_lien : The _id_concurrence_lien of the competitor to retrieve.
  ///
  /// Returns:
  ///   A Etab Competitor object, or null if no object is found.
  EtabCompetitor? getEtabCompetitorByLink(int _id_concurrence_lien) {
    final query = etabCompetitorBox
        .query(EtabCompetitor_.id_concurrence_lien.equals(_id_concurrence_lien))
        .build();
    return query.findFirst();
  }

  int getEtabCompetitorsCount() {
    return etabCompetitorBox.count();
  }

  /// VISITE ------------------------------------------------------------------
  ///

  Stream<List<Visit>> getVisitStream() {
    // Query for all visites, sorted by their date.
    // https://docs.objectbox.io/queries
    final builder =
        visitBox.query().order(Visit_.date_visite, flags: Order.descending);
    // Build and watch the query,
    // set triggerImmediately to emit the query immediately on listen.
    return builder
        .watch(triggerImmediately: true)
        // Map it to a list of notes to be used by a StreamBuilder.
        .map((query) => query.find());
  }

  List<Visit> getAllVisit() {
    // Query for all visites, sorted by their date.
    final builder = visitBox
        .query()
        .order(Visit_.date_visite, flags: Order.descending)
        .build();
    // Execute the query and return the result.
    return builder.find();
  }

  List<Visit> getTodayVisit() {
    final now = DateTime.now();

    // Get the start and end of the current day as timestamps.
    final startOfDay =
        DateTime(now.year, now.month, now.day).millisecondsSinceEpoch;
    final endOfDay = DateTime(now.year, now.month, now.day, 23, 59, 59)
        .millisecondsSinceEpoch;

    // Query for all visits that occur on the current day and have not been abandoned.
    final builder = visitBox
        .query(Visit_.date_visite.between(startOfDay, endOfDay) &
            Visit_.abandon.equals(0))
        .order(Visit_.date_visite, flags: Order.descending)
        .build();

    // Execute the query and return the result.
    return builder.find();
  }

  List<Visit> getPreviousVisit() {
    // Get the previous date at midnight.
    final now = DateTime.now();
    final midnight = DateTime(now.year, now.month, now.day);

    // Convert the date to an integer.
    final millisecondsSinceEpoch = midnight.millisecondsSinceEpoch;

    // Query for all visits that match the date and abandonment criteria, sorted by their date.
    final builder = visitBox
        .query(
          Visit_.date_visite.lessThan(millisecondsSinceEpoch) &
              Visit_.abandon.equals(0),
        )
        .order(Visit_.date_visite, flags: Order.descending)
        .build();

    // Execute the query and return the result.
    return builder.find();
  }

  // A function that converts a response body list into a List<Visite>.
  List<Visit> parseVisit(List responseDataList) {
    final parsed = responseDataList.cast<Map<String, dynamic>>();
    return parsed.map<Visit>((json) => Visit.fromJson(json)).toList();
  }

  // TODO : Enregistre urlphotoprincipale sur le disque pour l'avoir en cache ?
  Future<void> addVisits(List<dynamic> _listVisits) =>
      store.runInTransactionAsync(
          TxMode.write, _addVisitsInTx, parseVisit(_listVisits));

  static void _addVisitsInTx(Store store, _Visits) {
    store.box<Visit>().putMany(_Visits);
  }

  Future<void> addVisit(
          int _id_visite,
          DateTime _date_visite,
          DateTime _date_debut,
          DateTime _date_fin,
          DateTime _date_validation,
          String _type_visite,
          String _title,
          bool _allDay,
          String _start,
          int _id_distrib_visite,
          int _id_etab,
          int _abandon,
          String _end,
          String _url_photo_principale,
          String _langage) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addVisitInTx,
          Visit(
              id_visite: _id_visite,
              date_visite: _date_visite,
              date_debut: _date_debut,
              date_fin: _date_fin,
              date_validation: _date_validation,
              type_visite: _type_visite,
              title: _title,
              allDay: _allDay,
              id_distrib_visite: _id_distrib_visite,
              id_etab: _id_etab,
              abandon: _abandon,
              url_photo_principale: _url_photo_principale,
              langage: _langage));

  static void _addVisitInTx(Store store, _Visite) {
    store.box<Visit>().put(_Visite);
  }

  Future<void> syncVisits(List<dynamic> _listVisits) async {
    final toSyncId_visite =
        _listVisits.map<int>((v) => v['id_visite']).toList();
    final existingVisitsQuery =
        visitBox.query(Visit_.id_visite.oneOf(toSyncId_visite));
    final existingVisits = existingVisitsQuery.build().find();
    final existingVisitIds =
        existingVisits.map<int>((v) => v.id_visite).toList();

    final newVisits = _listVisits
        .where((v) => !existingVisitIds.contains(v['id_visite']))
        .toList();
    final updatedVisits = _listVisits
        .where((v) => existingVisitIds.contains(v['id_visite']))
        .toList();

    newVisits.forEach((v) {
      visitBox.put(Visit.fromJson(v));
    });

    updatedVisits.forEach((v) {
      final existingVisit =
          existingVisits.firstWhere((ev) => ev.id_visite == v['id_visite']);
      final updatedVisit = Visit.fromJson(v)..id = existingVisit.id;
      visitBox.put(updatedVisit);
    });
  }

  int getVisitCount() {
    return visitBox.count();
  }

  /// VISIT TAG ---------------------------------------------------------------
  ///

  // converts a response body list into a List<VisitTag>.
  List<VisitTag> parseVisitTags(List responseDataList) {
    final parsed = responseDataList.cast<Map<String, dynamic>>();
    return parsed.map<VisitTag>((json) => VisitTag.fromJson(json)).toList();
  }

  Future<void> addVisitTags(List<dynamic> _listVisitTags) =>
      store.runInTransactionAsync(
          TxMode.write, _addVisitTagsInTx, parseVisitTags(_listVisitTags));

  static void _addVisitTagsInTx(Store store, _VisitTags) {
    store.box<VisitTag>().putMany(_VisitTags);
  }

  /// save new tag for a distributor / langage
  Future<void> addVisitTag(
    int id_visite_tag,
    int _id_distrib,
    String _libelle,
    String _langage,
  ) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addVisitTagInTx,
          VisitTag(
            id_visite_tag: id_visite_tag,
            id_distrib: _id_distrib,
            libelle: _libelle,
            langage: _langage,
          ));

  static void _addVisitTagInTx(Store store, _VisitTag) {
    store.box<VisitTag>().put(_VisitTag);
  }

  List<String> getVisitTagsLabels(int distribId, String langage) {
    final query = visitTagBox
        .query(VisitTag_.id_distrib.equals(distribId) &
            VisitTag_.langage.equals(langage))
        //.order(VisitTag_.libelle)
        .build();
    PropertyQuery<String> pq = query.property(VisitTag_.libelle);
    pq.distinct = true;
    pq.caseSensitive = false;
    return pq.find();
  }

  int getVisitTagCount() {
    return visitTagBox.count();
  }

  /// PHOTO --------------------------------------------------------------------
  ///
  ///

  Future<List<VisitPhoto>> addPhotos(List<VisitPhoto> photos) async {
    final addedPhotos = await visitPhotoBox.putAndGetManyAsync(photos);
    return addedPhotos;
  }

  Future<void> addPhoto(
          int id_visite, int id_photo_typologie, String image_name) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addPhotoInTx,
          VisitPhoto(
              id_visite: id_visite,
              id_photo_typologie: id_photo_typologie,
              image_name: image_name));

  static void _addPhotoInTx(Store store, _Photo) {
    // Perform ObjectBox operations that take longer than a few milliseconds
    // here. To keep it simple, this example just puts a single object.
    store.box<VisitPhoto>().put(_Photo);
  }

  List<VisitPhoto> getAllVisitTypologyPhotos(
      int id_visite, int _id_photo_typologie) {
    // Query for all photos, sorted by their date.
    // https://docs.objectbox.io/queries
    final query = visitPhotoBox
        .query(VisitPhoto_.id_visite.equals(id_visite) &
            VisitPhoto_.id_photo_typologie.equals(_id_photo_typologie))
        .order(VisitPhoto_.date_photo, flags: Order.descending)
        .build();
    return query.find();
  }

  // Retrieves a VisitPhoto object from the ObjectBox database with the specified visit ID.
  ///
  /// Parameters:
  /// _id_visite: The ID of the visit the photo belongs to.
  ///
  /// Returns:
  /// A VisitPhoto object, or null if no object is found.
  List<VisitPhoto> getAllVisitPhotosByVisit(int id_visite) {
    final query =
        visitPhotoBox.query(VisitPhoto_.id_visite.equals(id_visite)).build();
    return query.find();
  }

  /// Retrieves all VisitPhoto objects from the ObjectBox database.
  ///
  /// Parameters:
  /// None.
  ///
  /// Returns:
  /// A list of VisitPhoto objects, or an empty list if no objects are found.
  List<VisitPhoto> getAllVisitPhotos() {
    final query = visitPhotoBox.query().build();
    return query.find();
  }

  /// Retrieves a Photo object from the ObjectBox database with the specified ID.
  ///
  /// Parameters:
  ///   id_photo: The ID of the photo to retrieve.
  ///
  /// Returns:
  ///   A Photo object, or null if no object is found.
  VisitPhoto? getPhotoById(int _id) {
    final photo = visitPhotoBox.get(_id);
    return photo;
  }

  Stream<List<VisitPhoto>> getPhotos() {
    // Query for all photos, sorted by their date.
    // https://docs.objectbox.io/queries
    final builder = visitPhotoBox
        .query()
        .order(VisitPhoto_.date_photo, flags: Order.descending);
    // Build and watch the query,
    // set triggerImmediately to emit the query immediately on listen.
    return builder
        .watch(triggerImmediately: true)
        // Map it to a list of notes to be used by a StreamBuilder.
        .map((query) => query.find());
  }

  /// Removes a VisitPhoto object from the ObjectBox database with the specified ID.
  ///
  /// Parameters:
  /// id: The ID of the VisitPhoto object to remove.
  ///
  /// Returns:
  /// None.
  void delPhotoById(int id) {
    if (visitPhotoBox.remove(id)) {
      print("Supression photo :${id}");
    } else
      print("Supression photo :${id} KO");
  }

  /// Removes a VisitPhoto object from the ObjectBox database with the specified ID.
  ///
  /// Parameters:
  /// id: The NAME of the VisitPhoto object to remove.
  ///
  /// Returns:
  /// None.
  void delPhotoByName(String _name) {
    final query =
        visitPhotoBox.query(VisitPhoto_.image_name.equals(_name)).build();
    final results = query.find();
    if (results.isNotEmpty) {
      final photoToDelete = results.first;
      visitPhotoBox.remove(photoToDelete.id);
    }
  }

  int getVisitPhotoCount(int _id_visite) {
    final builder =
        visitPhotoBox.query(VisitPhoto_.id_visite.equals(_id_visite)).build();
    return builder.count();
  }

  int getVisitTypologiePhotoCount(int _id_visite, int _id_photo_typologie) {
    final builder = visitPhotoBox
        .query(VisitPhoto_.id_visite.equals(_id_visite) &
            VisitPhoto_.id_photo_typologie.equals(_id_photo_typologie))
        .build();
    return builder.count();
  }

  void putPhotoIdMP4(int photoId, int id_photo_mp4) {
    final photo = visitPhotoBox.get(photoId);

    if (photo != null) {
      final updatedPhoto = photo.copyWith(id_photo_mp4: id_photo_mp4);
      visitPhotoBox.put(updatedPhoto);
    }
  }

  void putPhotoTypologie(int photoId, int typologieId) {
    final photo = visitPhotoBox.get(photoId);

    if (photo != null) {
      final updatedPhoto = photo.copyWith(id_photo_typologie: typologieId);
      visitPhotoBox.put(updatedPhoto);
    }
  }

  void putPhotoTags(int photoId, List<String> tags) {
    final photo = visitPhotoBox.get(photoId);

    if (photo != null) {
      final updatedPhoto = photo.copyWith(tags: tags.join(","));
      visitPhotoBox.put(updatedPhoto);
    }
  }

  void putPhotoVisibilities(int photoId, List<String> visibilities) {
    final photo = visitPhotoBox.get(photoId);

    if (photo != null) {
      final updatedPhoto = photo.copyWith(
          photo_principale: visibilities.contains('principal') ? 1 : 0,
          photo_privee: visibilities.contains('private') ? 1 : 0);

      visitPhotoBox.put(updatedPhoto);
    }
  }

  void putPhotoCompetitor(int photoId, int competitorId) {
    final photo = visitPhotoBox.get(photoId);

    if (photo != null) {
      final updatedPhoto = photo.copyWith(id_concurrence_lien: competitorId);
      visitPhotoBox.put(updatedPhoto);
    }
  }

  /* remettre les principal à zero
  final queryBuilder = box.query(VisitPhoto_.Visit_id.equals(idVisite) & VisitPhoto_.photo_principale.equals(1));
  final updatedPhotos = queryBuilder.build().find();
  updatedPhotos.forEach((photo) {
    photo.photo_principale = 0;
  });
    box.putMany(updatedPhotos);
  */

  /// PHOTO TYPOLOGY -----------------------------------------------------------
  ///

  List<PhotoTypology> parsePhotoTypologies(List responseDataList) {
    final parsed = responseDataList.cast<Map<String, dynamic>>();
    return parsed
        .map<PhotoTypology>((json) => PhotoTypology.fromJson(json))
        .toList();
  }

  Future<void> addPhotoTypologies(List<dynamic> _listPhotoTypologies) =>
      store.runInTransactionAsync(TxMode.write, _addPhotoTypologiesInTx,
          parsePhotoTypologies(_listPhotoTypologies));

  static void _addPhotoTypologiesInTx(Store store, _PhotoTypologies) {
    store.box<PhotoTypology>().putMany(_PhotoTypologies);
  }

  Future<void> addPhotoTypology(
          int id_photo_typologie, String libelle, int ordre) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addPhotoTypologyInTx,
          PhotoTypology(
              id_photo_typologie: id_photo_typologie,
              libelle: libelle,
              ordre: ordre));

  static void _addPhotoTypologyInTx(Store store, _PhotoTypology) {
    // Perform ObjectBox operations that take longer than a few milliseconds
    // here. To keep it simple, this example just puts a single object.
    store.box<PhotoTypology>().put(_PhotoTypology);
  }

  List<String> getPhotoTypologiesLabels() {
    final query = PhotoTypologyBox.query().order(PhotoTypology_.ordre).build();
    final photoTypologies = query.find();
    return photoTypologies
        .map((photoTypology) => photoTypology.libelle)
        .toList();
  }

  List<PhotoTypology> getPhotoTypologiesList() {
    final query = PhotoTypologyBox.query().order(PhotoTypology_.ordre).build();
    final photoTypologies = query.find();

    return photoTypologies.toList();
  }

  Stream<List<PhotoTypology>> getPhotoTypologies() {
    // Query for all Typologies, sorted by their order.
    // https://docs.objectbox.io/queries
    final builder = PhotoTypologyBox.query().order(PhotoTypology_.ordre);
    // Build and watch the query,
    // set triggerImmediately to emit the query immediately on listen.
    return builder
        .watch(triggerImmediately: true)
        // Map it to a list of notes to be used by a StreamBuilder.
        .map((query) => query.find());
  }

  /// LOG ----------------------------------------------------------------------
  ///
  Future<void> addLog(String type, String module, String libelle, int duree) =>
      store.runInTransactionAsync(
          TxMode.write,
          _addLogInTx,
          Log(
            type,
            module,
            libelle,
            duree,
          ));

  static void _addLogInTx(Store store, _Log) {
    // Perform ObjectBox operations that take longer than a few milliseconds
    // here. To keep it simple, this example just puts a single object.
    store.box<Log>().put(_Log);
  }

  Stream<List<Log>> getLogs() {
    // Query for all logs, sorted by their date.
    // https://docs.objectbox.io/queries
    final builder = logBox.query().order(Log_.date, flags: Order.descending);
    // Build and watch the query,
    // set triggerImmediately to emit the query immediately on listen.
    return builder
        .watch(triggerImmediately: true)
        // Map it to a list of notes to be used by a StreamBuilder.
        .map((query) => query.find());
  }
}
