namespace Turntable {
	public static Mpris.Manager mpris_manager;
	public const int PROGRESS_UPDATE_TIME = 1000; // it was 250ms, turns out it updates every second?
	public static Application application;
	public static bool debug_enabled = false;
	public static bool is_rtl = false;
	#if SANDBOXED
		public static string[]? desktop_file_dirs = null;
	#endif

	//  public static Utils.Cache custom_color_cache;
	public static Utils.Settings settings;
	public static bool headless = false;
	#if SCROBBLING
		public static Scrobbling.Manager scrobbling_manager;
		public static Scrobbling.AccountManager account_manager;
		public static bool cli_list_clients = false;
		public static string cli_client_id_scrobble;
		public static bool cli_mode = false;
		public static NetworkMonitor network_monitor;
	#endif
	public class Application : Adw.Application {
		#if SCROBBLING
			[Signal (detailed = true)]
			public signal void token_received (Scrobbling.Manager.Provider provider, string token);
			public bool batch_in_progress { get; set; default = false; }

			public const GLib.OptionEntry[] APP_OPTIONS = {
				{ "list-clients", 'l', 0, GLib.OptionArg.NONE, ref cli_list_clients, "List all currently available MPRIS clients (ID - Name)", null },
				{ "client", 'c', 0, GLib.OptionArg.STRING, ref cli_client_id_scrobble, "MPRIS client ID to scrobble", "CLIENT-ID" },
				{ "headless", 0, 0, GLib.OptionArg.NONE, ref headless, "Do not show main window on start", null },
				{ null }
			};
		#endif

		private const GLib.ActionEntry[] APP_ENTRIES = {
			{ "about", on_about_action },
			{ "new-window", on_new_window },
			{ "preferences", on_preferences },
			{ "quit", quit }
		};

		string troubleshooting = "os: %s %s\nprefix: %s\nsandboxed: %s\nversion: %s (%s)\ngtk: %u.%u.%u (%d.%d.%d)\nlibadwaita: %u.%u.%u (%d.%d.%d)\ndebug-logs: %s\nscrobbling: %s".printf (
			GLib.Environment.get_os_info ("NAME"), GLib.Environment.get_os_info ("VERSION"),
			Build.PREFIX,
			#if SANDBOXED
				"true",
			#else
				"false",
			#endif
			Build.VERSION, Build.PROFILE,
			Gtk.get_major_version (), Gtk.get_minor_version (), Gtk.get_micro_version (),
			Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION,
			Adw.get_major_version (), Adw.get_minor_version (), Adw.get_micro_version (),
			Adw.MAJOR_VERSION, Adw.MINOR_VERSION, Adw.MICRO_VERSION,
			debug_enabled.to_string (),
			#if SCROBBLING
				"true\nlibsoup: %u.%u.%u (%d.%d.%d)\njson-glib: %d.%d.%d\nlibsecret: %d.%d.%d\nCLI: %s".printf (
					Soup.get_major_version (), Soup.get_minor_version (), Soup.get_micro_version (),
					Soup.MAJOR_VERSION, Soup.MINOR_VERSION, Soup.MICRO_VERSION,
					Json.MAJOR_VERSION, Json.MINOR_VERSION, Json.MICRO_VERSION,
					Secret.MAJOR_VERSION, Secret.MINOR_VERSION, Secret.MICRO_VERSION,
					cli_mode.to_string ()
				)
			#else
				"false"
			#endif
		);

		construct {
			application_id = Build.DOMAIN;
			flags = ApplicationFlags.HANDLES_OPEN;
		}

		protected override void startup () {
			base.startup ();

			var lines = troubleshooting.split ("\n");
			foreach (unowned string line in lines) {
				debug (line);
			}
			Gtk.Window.set_default_icon_name (Build.DOMAIN);
			Adw.init ();

			is_rtl = Gtk.Widget.get_default_direction () == Gtk.TextDirection.RTL;
			//  custom_color_cache = Utils.Cache ();
			settings = new Utils.Settings ();
			#if SCROBBLING
				account_manager = new Scrobbling.AccountManager ();
				scrobbling_manager = new Scrobbling.Manager ();
			#endif

			this.add_action_entries (APP_ENTRIES, this);
			this.set_accels_for_action ("app.preferences", {"<primary>comma"});
			this.set_accels_for_action ("app.quit", {"<primary>q"});
			this.set_accels_for_action ("app.new-window", {"<primary>n"});

			#if SANDBOXED
				var display = Gdk.Display.get_default ();
				if (display != null) {
					string home = GLib.Environment.get_home_dir ();
					var theme = Gtk.IconTheme.get_for_display (display);
					theme.add_search_path (@"$home/.local/share/icons");
					theme.add_search_path (@"$home/.local/share/flatpak/exports/share/icons");
					theme.add_search_path ("/var/lib/snapd/desktop/icons");
					theme.add_search_path ("/var/lib/flatpak/exports/share/icons");
				}
			#endif
		}

		public static int main (string[] args) {
			#if SCROBBLING
				try {
					var opt_context = new OptionContext ("- Options");
					opt_context.set_summary (@"CLI MPRIS Scrobbler. Accounts can be configured in GUI mode. --list-clients exists for convenience; the --client doesn't need to exist before running. $(Build.NAME) will keep track of MPRIS client changes and automatically connect to it when it appears. GUI mode settings, like the MBID one, affect CLI mode.");
					opt_context.set_description (@"Send bug reports to $(Build.ISSUES_WEBSITE), including logs generated by running $(Build.NAME) with G_MESSAGES_DEBUG=Turntable");
					opt_context.add_main_entries (APP_OPTIONS, null);
					opt_context.parse (ref args);
				} catch (GLib.OptionError e) {
					warning (e.message);
				}
			#endif

			Intl.setlocale (LocaleCategory.ALL, "");
			Intl.bindtextdomain (Build.GETTEXT_PACKAGE, Build.LOCALEDIR);
			Intl.bind_textdomain_codeset (Build.GETTEXT_PACKAGE, "UTF-8");
			Intl.textdomain (Build.GETTEXT_PACKAGE);

			debug_enabled = !GLib.Log.writer_default_would_drop (GLib.LogLevelFlags.LEVEL_DEBUG, "Turntable");
			GLib.Environment.unset_variable ("GTK_THEME");

			#if SCROBBLING
				cli_mode = cli_list_clients || cli_client_id_scrobble != null;
			#endif

			#if SANDBOXED
				#if SCROBBLING
					if (!cli_mode)
				#endif // vala-lint=block-opening-brace-space-before
				{
					string[] temp_dirs = {};
					string home = GLib.Environment.get_home_dir ();
					string[] def_dirs = {
						"/usr/share/applications",
						@"$home/.local/share/applications",
						"/var/lib/flatpak/exports/share/applications",
						@"$home/.local/share/flatpak/exports/share/applications",
						"/var/lib/snapd/desktop/applications"
					};

					foreach (string path in def_dirs) {
						if (GLib.FileUtils.test (path, GLib.FileTest.IS_DIR)) {
							temp_dirs += path;
						}
					}

					desktop_file_dirs = temp_dirs;
				}
			#endif

			mpris_manager = new Mpris.Manager ();

			#if SCROBBLING
				network_monitor = NetworkMonitor.get_default ();
				if (cli_mode) return (new Utils.CLI ()).run ();
			#endif

			application = new Application ();
			return application.run (args);
		}

		bool activated = false;
		public override void activate () {
			base.activate ();

			var win = this.active_window ?? new Turntable.Views.Window (this);
			if (!headless || (headless && activated)) win.present ();
			#if SCROBBLING
				if (!activated) account_manager.load ();
			#endif
			activated = true;
		}

		const string[] DEVELOPERS = { "Evangelos “GeopJr” Paterakis" };
		const string[] DESIGNERS = { "Evangelos “GeopJr” Paterakis" };
		const string[] ARTISTS = { "Evangelos “GeopJr” Paterakis" };
		private void on_about_action () {
			var about = new Adw.AboutDialog () {
				application_name = Build.NAME,
				application_icon = Build.DOMAIN,
				developer_name = DEVELOPERS[0],
				version = Build.VERSION,
				developers = DEVELOPERS,
				artists = ARTISTS,
				designers = DESIGNERS,
				debug_info = troubleshooting,
				debug_info_filename = @"$(Build.NAME).txt",
				copyright = @"© 2025 $(DEVELOPERS[0])",
				// translators: Name <email@domain.com> or Name https://website.example
				translator_credits = _("translator-credits")
			};

			about.add_link (_("Donate"), Build.DONATE_WEBSITE);
			about.add_link (_("Translate"), Build.TRANSLATE_WEBSITE);
			#if SCROBBLING
				// translators: Shown in the about dialog as a tip. Leave markup as is (<b>, \n)
				about.comments = _("<b>Best Practices for Scrobbling</b>\n• Avoid allowlisting non-curated MPRIS clients like Web Browsers and Video Players\n• Tag your music with the proper track, album and artist names\n• Check, fix and match your scrobbles regularly");
			#endif

			// translators: Application metainfo for the app "Archives". <https://gitlab.gnome.org/GeopJr/Archives/>
			about.add_other_app ("dev.geopjr.Archives", _("Archives"), _("Create and view web archives"));
			// translators: Application metainfo for the app "Calligraphy". <https://gitlab.gnome.org/GeopJr/Calligraphy>
			about.add_other_app ("dev.geopjr.Calligraphy", _("Calligraphy"), _("Turn text into ASCII banners"));
			// translators: Application metainfo for the app "Collision". <https://github.com/GeopJr/Collision>
			about.add_other_app ("dev.geopjr.Collision", _("Collision"), _("Check hashes for your files"));
			// translators: Application metainfo for the app "Tuba". <https://github.com/GeopJr/Tuba>
			about.add_other_app ("dev.geopjr.Tuba", _("Tuba"), _("Browse the Fediverse"));

			this.active_window.resizable = false;
			about.present (this.active_window);
			about.closed.connect (((Views.Window) this.active_window).make_resizable);

			GLib.Idle.add (() => {
				var style = Utils.Celebrate.get_celebration_css_class (new GLib.DateTime.now ());
				if (style != "") about.add_css_class (style);
				return GLib.Source.REMOVE;
			});
		}

		private void on_new_window () {
			debug ("New Window");
			(new Turntable.Views.Window (this)).present ();
		}

		private void on_preferences () {
			if (this.active_window == null) return;

			this.active_window.resizable = false;
			var perfs = new Turntable.Views.Preferences ();
			perfs.present (this.active_window);
			perfs.closed.connect (((Views.Window) this.active_window).make_resizable);
		}

		private Xdp.Portal? portal = null;
		public async bool request_autostart (bool autostart, bool hidden) throws GLib.Error {
			if (cli_mode || this.active_window == null) return false;
			GLib.GenericArray<weak string> cmd = new GLib.GenericArray<weak string> ();
			cmd.add (Build.DOMAIN);
			if (autostart && hidden) cmd.add ("--headless");

			if (portal == null) portal = new Xdp.Portal ();
			return yield portal.request_background (
				Xdp.parent_new_gtk (this.active_window),
				// translators: message shown when requesting autostart or run in the background permissions
				//				the variable is the app name (Turntable)
				(autostart ? _("Allow %s to start when you log in") : _("Allow %s to run in the background")).printf (Build.NAME),
				cmd,
				autostart ? Xdp.BackgroundFlags.AUTOSTART : Xdp.BackgroundFlags.NONE,
				null
			);
		}

		public async void background_portal_message (string? message) {
			if (cli_mode || !Xdp.Portal.running_under_sandbox ()) return;

			if (portal == null) portal = new Xdp.Portal ();
			try {
				yield portal.set_background_status (message, null);
			} catch (GLib.Error e) {
				warning (@"Couldn't change the background portal message: $(e.message) $(e.code)");
			}
		}

		public override void open (File[] files, string hint) {
			if (!activated) activate ();

			debug ("Received open signal");
			foreach (File file in files) {
				string unparsed_uri = file.get_uri ();

				try {
					Uri uri = Uri.parse (unparsed_uri, UriFlags.ENCODED);
					string scheme = uri.get_scheme ();

					switch (scheme) {
						case "turntable":
							string? host = uri.get_host ();
							if (host == null) host = "";

							string down_host = host.down ();
							switch (down_host) {
								#if SCROBBLING
									case "lastfm":
									case "librefm":
										string? path = uri.get_path ();
										if (path == null || path == "") throw new Error.literal (-1, 3, @"$unparsed_uri doesn't have win id");
										if (!path.has_prefix ("/")) path = @"/$path";

										string[] path_parts = path.split ("/");
										if (path_parts.length < 2 || path_parts[1].length < 4) throw new Error.literal (-1, 3, @"$unparsed_uri doesn't have win id");
										string win_id = path_parts[1];

										string? query = uri.get_query ();
										if (query == null || query == "") throw new Error.literal (-1, 3, @"$unparsed_uri doesn't have query params");

										var uri_params = Uri.parse_params (query);
										if (!uri_params.contains ("token")) throw new Error.literal (-1, 3, @"$unparsed_uri doesn't have a 'token' query param");

										token_received[win_id] (
											down_host == "librefm"
												? Scrobbling.Manager.Provider.LIBREFM
												: Scrobbling.Manager.Provider.LASTFM,
											uri_params.get ("token")
										);
										break;
								#endif
								default:
									throw new Error.literal (-1, 3, @"$(Build.NAME) does not handle '$host'");
							}

							#if SCROBBLING
								break;
							#endif
						default:
							throw new Error.literal (-1, 3, @"$(Build.NAME) does not accept '$scheme://'");
					}
				} catch (GLib.Error e) {
					critical (@"Couldn't open $unparsed_uri: $(e.message)");
				}
			}
		}
	}
}
