
// Singleton
import 'dart:async';

import 'package:appwrite/appwrite.dart' as appwrite;
import 'package:appwrite/models.dart' as models;
import 'package:bus_infotainment/audio_cache.dart';
import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;

class LiveInformation {

  static final LiveInformation _singleton = LiveInformation._internal();

  factory LiveInformation() {
    return _singleton;
  }

  LiveInformation._internal();

  Future<void> Initialize() async {

    {
      // Load the bus sequences

      try {

        http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));

        busSequences = BusSequences.fromCSV(response.body);

        print("Loaded bus sequences from TFL");

      } catch (e) {

          String csv = await rootBundle.loadString("assets/datasets/bus-sequences.csv");

          busSequences = BusSequences.fromCSV(csv);

          print("Loaded bus sequences from assets");
      }

      if (auth.isAuthenticated()){
        print("Auth is authenticated");
        setupRealtime();
      } else {
        print("Auth is not authenticated");
        auth.onLogin.addListener((value) {
          setupRealtime();
        });
      }

    }

    refreshTimer();
  }


  Timer refreshTimer() => Timer.periodic(Duration(milliseconds: 50), (timer) {
    _handleAnnouncementQueue();
  });


  AudioWrapper audioPlayer = AudioWrapper();
  AnnouncementCache announcementCache = AnnouncementCache();
  List<AnnouncementQueueEntry> announcementQueue = [];
  DateTime lastAnnouncement = DateTime.now();
  EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
  String _currentAnnouncement = "*** NO MESSAGE ***";

  String get currentAnnouncement => _currentAnnouncement;
  void set currentAnnouncement(String value) {
    _currentAnnouncement = value;
  }

  void _handleAnnouncementQueue() async {

    int timerInterval = 50;

    // print("Handling announcement queue");
    if (audioPlayer.state != AudioWrapper_State.Playing) {
      if (announcementQueue.isNotEmpty) {

        AnnouncementQueueEntry announcement = announcementQueue.first;

        {
          DateTime now = DateTime.now();
          if (announcement.scheduledTime != null) {
            int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch);
            // print("Q Difference: ${milisecondDifference}");
            if (milisecondDifference <= timerInterval) {
              // Account for the time lost by the periodic timer
              await Future.delayed(Duration(milliseconds: timerInterval - milisecondDifference));
            } else {
              return;
            }
          }


        }


        // announcementDelegate.trigger(announcement);
        _currentAnnouncement = announcement.displayText;

        lastAnnouncement = DateTime.now();

        for (AudioWrapperSource source in announcement.audioSources) {

          Duration? duration = await audioPlayer.play(source);
          await Future.delayed(duration!);
          await Future.delayed(Duration(milliseconds: 150));
        }
        audioPlayer.stop();
        announcementQueue.remove(announcement);
        print("Queue length: ${announcementQueue.length}");
        print("Popped announcement queue");
      }
    }
  }

  void announceRouteVariant(BusRouteVariant routeVariant) async {
    if (routeVariant == null) {
      return;
    }
    String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";

    String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
    String audio_destination = routeVariant.busStops.last.getAudioFileName();

    print("Audio file: $audio_route");

    await announcementCache.loadAnnouncements([audio_route, audio_destination]);

    AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
    AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);

    queueAnnouncement(AnnouncementQueueEntry(
      displayText: display,
      audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
    ));

  }

  late BusSequences busSequences;
  BusRouteVariant? _currentRouteVariant;
  EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();

  void setRouteVariant(BusRouteVariant routeVariant) {
    _currentRouteVariant = routeVariant;
    announceRouteVariant(routeVariant);
    routeVariantDelegate.trigger(routeVariant);
  }

  BusRouteVariant? getRouteVariant() {
    return _currentRouteVariant;
  }

  void queueAnnouncement(AnnouncementQueueEntry announcement) {


    // Make sure the timestamp of the announcement is after the last announcement
    // If so, dont queue it
    // If timestamp is null, then skip this check
    if (announcement.timestamp != null && announcement.timestamp!.isBefore(lastAnnouncement)) {
      print("Announcement is too old");

      print("LastAnnouncement: $lastAnnouncement");
      print("Announcement: ${announcement.timestamp}");

      int difference = announcement.timestamp!.difference(lastAnnouncement).inMilliseconds;
      print("Difference: $difference");
      return;
    } else if (announcement.timestamp == null) {
      print("Announcement `${announcement.displayText}` does not have timestamp");
    }

    if (!announcement.sendToServer) {
      announcementQueue.add(announcement);
      return;
    }

    final databases = appwrite.Databases(auth.client);

    if (announcement is InformationAnnouncementEntry) {

      // 5 sedonds in the future
      DateTime scheduledTime = DateTime.now().add(Duration(seconds: 5));

      final document = databases.createDocument(
        documentId: appwrite.ID.unique(),
        databaseId: ApiConstants.INFO_Q_DATABASE_ID,
        collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
        data: {
          "ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement),
          "ScheduledTime": scheduledTime.toIso8601String(),
          "SessionID": sessionID,
        }
      );

      print("Queued manual announcement: ${announcement.shortName}");

    } else if (announcement is AnnouncementQueueEntry) {

    }

  }

  List<InformationAnnouncementEntry> manualAnnouncements = [
    InformationAnnouncementEntry(
      shortName: "Driver Change",
      informationText: "Driver Change",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/driverchange.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "No Standing Upr Deck",
      informationText: "No standing on the upper deck",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nostanding.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Face Covering",
      informationText: "Please wear a face covering!",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/facecovering.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Seats Upstairs",
      informationText: "Seats are available upstairs",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/seatsupstairs.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Bus Terminates Here",
      informationText: "Bus terminates here. Please take your belongings with you",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busterminateshere.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Bus On Diversion",
      informationText: "Bus on diversion. Please listen for further announcements",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busondiversion.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Destination Change",
      informationText: "Destination Changed - please listen for further instructions",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/destinationchange.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Wheelchair Space",
      informationText: "Wheelchair space requested",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace1.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Move Down The Bus",
      informationText: "Please move down the bus",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/movedownthebus.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Next Stop Closed",
      informationText: "The next bus stop is closed",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nextstopclosed.wav")],
    ),
    InformationAnnouncementEntry(
      shortName: "CCTV In Operation",
      informationText: "CCTV is in operation on this bus",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/cctvoperation.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Safe Door Opening",
      informationText: "Driver will open the doors when it is safe to do so",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/safedooropening.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Buggy Safety",
      informationText: "For your child's safety, please remain with your buggy",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/buggysafety.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Wheelchair Space 2",
      informationText: "Wheelchair priority space required",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace2.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Service Regulation",
      informationText: "Regulating service - please listen for further information",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/serviceregulation.mp3")],
    ),
    InformationAnnouncementEntry(
      shortName: "Bus Ready To Depart",
      informationText: "This bus is ready to depart",
      audioSources: [AudioWrapperAssetSource("audio/manual_announcements/readytodepart.mp3")],
    ),
  ];

  AuthAPI auth = AuthAPI();
  String sessionID = "65de648aa7f44684ecce";
  void updateServer() async {



    final databases = appwrite.Databases(auth.client);

    // final document = databases.updateDocument(
    //   databaseId: ApiConstants.INFO_DATABASE_ID,
    //   collectionId: ApiConstants.INFO_COLLECTION_ID,
    //   documentId: documentID,
    //   data: {
    //     "Display": _currentAnnouncement,
    //   }
    // );

    print("Updated server with announcement: $_currentAnnouncement");

  }
  void pullServer() async {

    if (auth.status == AuthStatus.UNAUTHENTICATED) {
      return;
    }

    final databases = appwrite.Databases(auth.client);

    // final document = await databases.getDocument(
    //   databaseId: ApiConstants.INFO_DATABASE_ID,
    //   collectionId: ApiConstants.INFO_COLLECTION_ID,
    //   documentId: documentID,
    // );

    // queueAnnouncement(AnnouncementQueueEntry(
    //   displayText: document.data['Display'],
    //   audioSources: [],
    //   sendToServer: false, // Don't send this back to the server, else we'll get an infinite loop
    // ));

    // print("Pulled announcement from server: ${document.data['Display']}");
  }

  bool purgeRunning = false;
  Future<void> deleteAllManualQueueEntries() async {

    if (purgeRunning) {
      return;
    }
    purgeRunning = true;

    final databases = appwrite.Databases(auth.client);
    int offset = 0;
    const int limit = 25; // Maximum number of documents that can be fetched at once

    while (true) {
      // Fetch a page of documents from the manual queue collection
      print("Deleting manual queue entries");
      final manual_q = await databases.listDocuments(
          databaseId: ApiConstants.INFO_Q_DATABASE_ID,
          collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
          queries: [
            appwrite.Query.search("SessionID", sessionID),
            appwrite.Query.limit(limit),
            appwrite.Query.offset(offset),
            appwrite.Query.orderDesc('\$createdAt')
          ]
      );

      // If there are no documents in the fetched page, break the loop
      if (manual_q.documents.isEmpty) {
        break;
      }

      // Delete each document in the fetched page
      for (models.Document doc in manual_q.documents) {
        await databases.deleteDocument(
          databaseId: ApiConstants.INFO_Q_DATABASE_ID,
          collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
          documentId: doc.$id,
        );
      }

      // Go to the next page
      offset += limit;
    }

    print("Deleted all manual queue entries");
  }

  void pullQueue() async {

    if (auth.status == AuthStatus.UNAUTHENTICATED) {
      return;
    }

    final databases = appwrite.Databases(auth.client);

    // Pull the manual queue
    final manual_q = await databases.listDocuments(
      databaseId: ApiConstants.INFO_Q_DATABASE_ID,
      collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
      queries: [
        appwrite.Query.search("SessionID", sessionID),
        appwrite.Query.limit(25),
        appwrite.Query.offset(0),
        appwrite.Query.orderDesc('\$createdAt')
      ]
    );

    List<AnnouncementQueueEntry> queue = [];

    for (models.Document doc in manual_q.documents) {
      int index = doc.data['ManualAnnouncementIndex'];

      InformationAnnouncementEntry announcement_clone = InformationAnnouncementEntry(
        shortName: manualAnnouncements[index].shortName,
        informationText: manualAnnouncements[index].displayText,
        audioSources: manualAnnouncements[index].audioSources,
        scheduledTime: doc.data["ScheduledTime"] != null ? DateTime.parse(doc.data["ScheduledTime"]) : null,
        timestamp: DateTime.parse(doc.$createdAt),
        sendToServer: false,
      );

      // sort the queue by timestamp, so the oldest announcements are at the front



      queue.add(announcement_clone);
    }




    for (AnnouncementQueueEntry entry in queue) {
      queueAnnouncement(entry);
    }

  }



  appwrite.RealtimeSubscription? manual_q_subscription;
  Future<void> setupRealtime() async {

    if (manual_q_subscription != null) {
      return;
    }

    // await deleteAllManualQueueEntries(); //todo

    print("Setting up realtime");

    // Websocket
    appwrite.Realtime realtime = appwrite.Realtime(auth.client);

    manual_q_subscription = realtime.subscribe(
      ['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'],
    );
    manual_q_subscription?.stream.listen((event) {
      print("Manual queue entry added");

      pullQueue();
    });

    print("Subscribed to servers");

    await Future.delayed(Duration(seconds: 90));

    manual_q_subscription?.close();
    manual_q_subscription = null;
    setupRealtime();

  }



}

class AnnouncementQueueEntry {
  final String displayText;
  final List<AudioWrapperSource> audioSources;
  bool sendToServer = true;
  DateTime? scheduledTime;
  DateTime? timestamp;

  AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp});
}

class ManualAnnouncementEntry extends AnnouncementQueueEntry {
  final String shortName;

  ManualAnnouncementEntry({
    required this.shortName,
    required String informationText,
    required List<AudioWrapperSource> audioSources,
    DateTime? scheduledTime,
    DateTime? timestamp,
    bool sendToServer = true,
  }) : super(
    displayText: informationText,
    audioSources: audioSources,
    sendToServer: sendToServer,
    scheduledTime: scheduledTime,
    timestamp: timestamp,
  );
}

class InformationAnnouncementEntry extends AnnouncementQueueEntry {
  final String shortName;

  InformationAnnouncementEntry({
    required this.shortName,
    required String informationText,
    required List<AudioWrapperSource> audioSources,
    DateTime? scheduledTime,
    DateTime? timestamp,
    bool sendToServer = true,
  }) : super(
    displayText: informationText,
    audioSources: audioSources,
    sendToServer: sendToServer,
    scheduledTime: scheduledTime,
    timestamp: timestamp,
  );
}

var abs = (int value) => value < 0 ? -value : value;