
// Singleton
import 'dart:async';
import 'dart:convert';

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/backend/modules/announcement.dart';
import 'package:bus_infotainment/backend/modules/commands.dart';
import 'package:bus_infotainment/backend/modules/synced_time.dart';
import 'package:bus_infotainment/backend/modules/tracker.dart';
import 'package:bus_infotainment/backend/modules/tube_info.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:bus_infotainment/workaround/keepalive_realtime.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:ntp/ntp.dart';
import 'package:permission_handler/permission_handler.dart';

class LiveInformation {

  static final LiveInformation _singleton = LiveInformation._internal();

  factory LiveInformation() {
    return _singleton;
  }

  LiveInformation._internal();

  Future<void> initialize() async {

    {
      // By default, load the bus sequences from the assets
      print("Loading bus sequences from assets");
      busSequences = BusSequences.fromCSV(
        await rootBundle.loadString("assets/datasets/destinations.json"),
        await rootBundle.loadString("assets/datasets/bus-sequences.csv")
      );
      print("Loaded bus sequences from assets");

      try {

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

        busSequences = BusSequences.fromCSV(
          await rootBundle.loadString("assets/datasets/destinations.json"),
          response.body
        );

        print("Loaded bus sequences from TFL");

      } catch (e) {
          print("Failed to load bus sequences from TFL. Using local copy.");
      }

      // Load tube stations
      print("Loading tube stations from assets");
      tubeStations = TubeStations.fromJson(json.decode(await rootBundle.loadString("assets/datasets/tube_stations.json")));
      print("Loaded tube stations from assets");
    }

    // Initialise modules
    syncedTimeModule = SyncedTimeModule();
    announcementModule = AnnouncementModule();

    initTrackerModule();

    print("Initialised LiveInformation");
    if (!auth.isAuthenticated()) {
      auth.loadAnonymousUser();
    }
  }

  Future<void> initTrackerModule() async {
    if (await Permission.location.isGranted) {
      trackerModule = TrackerModule();
    }
  }

  // Auth
  AuthAPI auth = AuthAPI(
    autoLoad: false,
  );
  String? roomCode;
  String? roomDocumentID;
  bool isHost = false;
  appwrite.RealtimeSubscription? _subscription;
  RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK


  // Modules
  // late CommandModule commandModule; This needs to be deprecated
  late BusSequences busSequences;
  late AnnouncementModule announcementModule;
  late SyncedTimeModule syncedTimeModule;
  late TrackerModule trackerModule;
  late TubeStations tubeStations;

  // Important variables
  BusRouteVariant? _currentRouteVariant;

  // Events
  EventDelegate<BusRouteVariant?> routeVariantDelegate = EventDelegate();

  // Internal methods





  Future<void> setRouteVariant(BusRouteVariant? routeVariant) async {

    if (routeVariant == null) {
      _currentRouteVariant = null;

      await announcementModule.queueAnnounceByAudioName(
        displayText: "*** NO MESSAGE ***",
      );

      routeVariantDelegate.trigger(null);
      return;
    }

    if (roomCode != null) {
      try {
        final client = auth.client;
        final databases = appwrite.Databases(client);

        final response = await databases.listDocuments(
            databaseId: "6633e85400036415ab0f",
            collectionId: "6633e85d0020f52f3771",
            queries: [
              appwrite.Query.search("SessionID", roomCode!)
            ]
        );

        final document = response.documents.first;

        // Check if the route is not the same
        if (document.data["CurrentRoute"] != routeVariant.busRoute.routeNumber || document.data["CurrentRouteVariant"] != routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)) {
          final updatedDocument = await databases.updateDocument(
              databaseId: "6633e85400036415ab0f",
              collectionId: "6633e85d0020f52f3771",
              documentId: document.$id,
              data: {
                "CurrentRoute": routeVariant.busRoute.routeNumber,
                "CurrentRouteVariant": routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant),
                "LastUpdater": auth.userID,
              }
          );
          print("Updated route on server");
        }
      } catch (e) {
        print("Failed to update route on server");
      }

    }
    Continuation:

    // Set the current route variant
    _currentRouteVariant = routeVariant;

    // Let everyone know that the route variant has been set/changed
    routeVariantDelegate.trigger(routeVariant);

    // Get all of the files that need to be cached
    List<String> audioFiles = [];

    for (BusRouteStop stop in routeVariant.busStops) {
      audioFiles.add(stop.getAudioFileName());
    }

    // Cache/Load the audio files
    await announcementModule
        .announcementCache
        .loadAnnouncementsFromBytes(await LiveInformation().announcementModule.getBundleBytes(), [
          ...audioFiles,
          if (!routeVariant.busRoute.routeNumber.toLowerCase().startsWith("ul"))
            "R_${routeVariant.busRoute.routeNumber}_001.mp3"
          else
            "R_RAIL_REPLACEMENT_SERVICE_001.mp3",

        ]
    );

    // Display the route variant
    announcementModule.queueAnnouncementByRouteVariant(routeVariant: routeVariant);

  }

  // Public methods

  BusRouteVariant? getRouteVariant() {
    return _currentRouteVariant;
  }

  Future<void> setRouteVariantQuery(String routeNumber, int routeVariantIndex) async {
    BusRoute route = busSequences.routes[routeNumber]!;
    BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];

    await setRouteVariant(
        routeVariant
    );
  }


// Multi device support

  Future<void> createRoom(String roomCode) async {
    print("Creating room with code $roomCode");

    // Update the room code
    this.roomCode = roomCode;

    // Enable host mode
    isHost = true;

    // Access the database
    final client = auth.client;
    final databases = appwrite.Databases(client);

    // Remove any existing documents
    final existingDocuments = await databases.listDocuments(
      databaseId: "6633e85400036415ab0f",
      collectionId: "6633e85d0020f52f3771",
      queries: [
        appwrite.Query.search("SessionID", roomCode)
      ]
    );
    for (var document in existingDocuments.documents) {
      await databases.deleteDocument(
        databaseId: "6633e85400036415ab0f",
        collectionId: "6633e85d0020f52f3771",
        documentId: document.$id
      );
    }

    // Create the document
    final document = await databases.createDocument(
      databaseId: "6633e85400036415ab0f",
      collectionId: "6633e85d0020f52f3771",
      documentId: appwrite.ID.unique(),
      data: {
        "SessionID": roomCode,
        "LastUpdater": auth.userID,
      }
    );

    // Listen for changes
    // { Breaks due to bug in appwrite
    //   final realtime = appwrite.Realtime(client);
    //
    //   if (_subscription != null) {
    //     _subscription!.close();
    //   }
    //
    //   _subscription = realtime.subscribe(
    //       ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}']
    //   );
    //   _subscription!.stream.listen(ServerListener);
    // }
    // Listen for changes
    if (_keepAliveConnection != null) {
      try {
        _keepAliveConnection!.close();
      } catch (e) {
        print("Failed to close connection... oh well");
      }
    }

    String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
    String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
    _keepAliveConnection = RealtimeKeepAliveConnection(
      channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
      onData: ServerListener,
      domain: domain,
      client: auth.client,
      onError: (e) {
        print("Workarround Error: $e");
      },
    );
    _keepAliveConnection!.initialize();


    // Update the room document ID
    roomDocumentID = document.$id;

    print("Created room with code $roomCode");
  }

  Future<void> joinRoom(String roomCode) async {
    print("Joining room with code $roomCode");

    // Disable host mode
    isHost = false;

    // Update the room code
    this.roomCode = roomCode;

    // Access the database
    final client = auth.client;
    final databases = appwrite.Databases(client);

    // Get the document
    final response = await databases.listDocuments(
        databaseId: "6633e85400036415ab0f",
        collectionId: "6633e85d0020f52f3771",
        queries: [
          appwrite.Query.search("SessionID", roomCode)
        ]
    );

    if (response.documents.isEmpty) {
      throw Exception("Room not found");
    }

    final document = response.documents.first;

    // Listen for changes
    // {
    //   final realtime = appwrite.Realtime(client);
    //
    //   if (_subscription != null) {
    //     _subscription!.close();
    //   }
    //
    //   _subscription = realtime.subscribe([
    //     'databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'
    //   ]);
    //
    //   _subscription!.stream.listen(ServerListener);
    // }
    // Listen for changes
    if (_keepAliveConnection != null) {
      try {
        _keepAliveConnection!.close();
      } catch (e) {
        print("Failed to close connection... oh well");
      }
    }

    String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
    String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
    _keepAliveConnection = RealtimeKeepAliveConnection(
      channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
      onData: ServerListener,
      domain: domain,
      client: auth.client,
      onError: (e) {
        print("Workarround Error: $e");
      },
    );
    _keepAliveConnection!.initialize();

    // Update the room document ID
    roomDocumentID = document.$id;

    // Get the current route
    try {
      String routeNumber = document.data["CurrentRoute"];
      int routeVariantIndex = document.data["CurrentRouteVariant"];

      await setRouteVariantQuery(routeNumber, routeVariantIndex);
      print("Set route to $routeNumber $routeVariantIndex");
    } catch (e) {
      print("Failed to set route");
    }

    print("Joined room with code $roomCode");
  }

  Future<void> leaveRoom() async {

    if (roomCode == null) {
      throw Exception("Not in a room");
    }

    if (isHost) {
      // Access the database
      final client = auth.client;
      final databases = appwrite.Databases(client);

      // Remove any existing documents
      final existingDocuments = await databases.listDocuments(
          databaseId: "6633e85400036415ab0f",
          collectionId: "6633e85d0020f52f3771",
          queries: [
            appwrite.Query.search("SessionID", roomCode!)
          ]
      );
      for (var document in existingDocuments.documents) {
        await databases.deleteDocument(
            databaseId: "6633e85400036415ab0f",
            collectionId: "6633e85d0020f52f3771",
            documentId: document.$id
        );
      }
    }

    roomCode = null;
    roomDocumentID = null;
    isHost = false;

    _keepAliveConnection?.close();
    _keepAliveConnection = null;

    // Reset stuff
    setRouteVariant(null);
  }

  String? lastCommand;
  Future<void> ServerListener(appwrite.RealtimeMessage response) async {
    print("Session update");

    // Only do something if the document was created or updated
    if (!(response.events.first.contains("create") || response.events.first.contains("update"))) {
      return;
    }

    // Get the user that caused the event

    String senderID = response.payload["LastUpdater"];
    // If the sender is the same as the client, then ignore the event
    if (senderID == auth.userID) {
      print("Ignoring event");
      return;
    }

    // Check to see if the commands are updated

    try {
      // Get the new route
      String routeNumber = response.payload["CurrentRoute"];
      int routeVariantIndex = response.payload["CurrentRouteVariant"];

      // If the route arent the same, then update the route
      if (routeNumber != _currentRouteVariant!.busRoute.routeNumber || routeVariantIndex != _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!)) {
        // Set the route
        await setRouteVariantQuery(routeNumber, routeVariantIndex);

        // announce the route
        // announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!);
      }
    } catch (e) {
      print("Failed to set route");
    }

    // Execute the command
    List<String> commands = response.payload["Commands"].cast<String>();

    String? command = commands.last;

    if (command == lastCommand) {
      return;
    }
    lastCommand = command;

    List<String> commandParts = _splitCommand(command);
    String commandName = commandParts[0];

    if (commandName == "announce") {
      print("Announce command received");
      String mode = commandParts[1];

      print ("Command: $command");

      if (mode == "info") {
        print("Announce info command received");
        announcementModule.queueAnnouncementByInfoIndex(
          sendToServer: false,
          infoIndex: int.parse(commandParts[2]),
          scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[3])),
        );
      } else if (mode == "dest") {
        print("Announce dest command received");

        String routeNumber = commandParts[2];
        int routeVariantIndex = int.parse(commandParts[3]);

        announcementModule.queueAnnouncementByRouteVariant(
          sendToServer: false,
          routeVariant: busSequences.routes[routeNumber]!.routeVariants.values.toList()[routeVariantIndex],
          scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[4])),
        );

      } else if (mode == "manual") {
        print("Announce manual command received");

        final displayText = commandParts[2];

        List<String> audioFileNames = commandParts.sublist(3);
        try {
          if (int.parse(audioFileNames.last) != null) {
            audioFileNames.removeLast();
          }
        } catch (e) {}

        DateTime scheduledTime = LiveInformation().syncedTimeModule.Now().add(Duration(seconds: 1));
        try {
          if (int.parse(commandParts.last) != null) {
            scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts.last));
          }
        } catch (e) {}

        announcementModule.queueAnnounceByAudioName(
            displayText: displayText,
            audioNames: audioFileNames,
            scheduledTime: scheduledTime,
            sendToServer: false
        );

      }
    }
  }

  String _extractId(String input) {
    RegExp regExp = RegExp(r'\("user:(.*)"\)');
    Match match = regExp.firstMatch(input)!;
    return match.group(1)!;
  }

  Future<void> SendCommand(String command) async {

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

    final response = await databases.listDocuments(
        databaseId: "6633e85400036415ab0f",
        collectionId: "6633e85d0020f52f3771",
        queries: [
          appwrite.Query.search("SessionID", roomCode!)
        ]
    );

    List<String> pastCommands = [];

    response.documents.first.data["Commands"].forEach((element) {
      pastCommands.add(element);
    });

    pastCommands.add(command);

    final document = await databases.updateDocument(
        databaseId: "6633e85400036415ab0f",
        collectionId: "6633e85d0020f52f3771",
        documentId: roomDocumentID!,
        data: {
          "Commands": pastCommands,
          "LastUpdater": auth.userID,
        }
    );
  }

  List<String> _splitCommand(String command) {
    var regex = RegExp(r'([^\s"]+)|"([^"]*)"');
    var matches = regex.allMatches(command);
    return matches.map((match) => match.group(0)!.replaceAll('"', '')).toList();
  }



  /*
  Everything under this will be considered legacy code
   */



}

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 NamedAnnouncementQueueEntry extends AnnouncementQueueEntry {
  final String shortName;

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

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