using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataUtils;
using AppxPackage;
using System.IO;
using static AppxPackage.PackageManager;
using Newtonsoft.Json;
using Win32;

namespace PkgCLI
{
	class Program
	{
		public delegate _I_HResult PackageOperation (string filePath, IEnumerable<string> depUris, DeploymentOptions options, PackageProgressCallback progress = null);
		static readonly string [] helpArgs = new string [] {
			"/?",
			"-?",
			"help",
			"/help",
			"-help",
			"/h",
			"-h"
		};
		static bool IsHelpParam (string arg) => helpArgs.Contains (arg.Normalize ());
		static void PrintVersion ()
		{
			var verFilePath = Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "version");
			var verFileInst = new _I_File (verFilePath);
			var verstr = verFileInst.Content?.Trim () ?? "0.0.0.1";
			Console.WriteLine (
				$"Package Manager CLI [Version {verstr}]\n(C) Windows Modern. All rights reserved."
			);
		}
		static void PrintTotalHelp ()
		{
			Console.WriteLine (@"
Usage:
    pkgcli <command> [arguments]

Commands:
    /install        Install a package.
    /update         Update a package (previous version must be installed).
    /register       Register an app manifest file.
    /stage          Stage a package.
    /remove         Remove the package.
    /read           Read package information.
    /get            List all installed apps.
    /find           Find installed apps.
    /active         Launch an app.
    /config         Configure settings (omit to show current config).
    /version        Display version information.
    /encoding       Set console output encoding. With other commands.
    /?              Show this help.
");
		}
		public static bool IsFilePathInList (List<string> filelist, string file)
		{
			foreach (var f in filelist)
			{
				if (f.NEquals (file)) return true;
			}
			return false;
		}
		public static void AddFiles (List<string> filelist, string file)
		{
			if (string.IsNullOrWhiteSpace (file)) return;
			if (!File.Exists (file)) file = Path.Combine (Environment.CurrentDirectory, file);
			if (!File.Exists (file)) return;
			var ext = Path.GetExtension (file);
			if (ext.NEquals (".txt"))
			{
				var lines = File.ReadAllLines (file);
				foreach (var l in lines)
				{
					if (string.IsNullOrWhiteSpace (l)) continue;
					var line = l.Trim ();
					if (!File.Exists (l)) line = Path.Combine (Path.GetDirectoryName (file), l);
					if (!File.Exists (line)) continue;
					if (!IsFilePathInList (filelist, line))
						filelist.Add (line);
				}
			}
			else if (!IsFilePathInList (filelist, file)) filelist.Add (file);
		}
		public static void AddNStringItems (List<string> strlist, string str)
		{
			var find = false;
			foreach (var l in strlist)
				if (l.NEquals (str))
				{
					find = true;
					break;
				}
			if (!find) strlist.Add (str);
		}
		public static void ToFormatString (PMPackageInfo pkg, IEnumerable<string> filter)
		{
			if (pkg == null) return;
			var labels = new List<string> ();
			var values = new List<string> ();

			var id = pkg.Identity;
			labels.Add ("Identity:Name"); values.Add (id.Name);
			labels.Add ("Identity:Publisher"); values.Add (id.Publisher);
			labels.Add ("Identity:FamilyName"); values.Add (id.FamilyName);
			labels.Add ("Identity:FullName"); values.Add (id.FullName);
			labels.Add ("Identity:PublisherId"); values.Add (id.PublisherId);
			labels.Add ("Identity:ResourceId"); values.Add (id.ResourceId);
			labels.Add ("Identity:Version"); values.Add (id.Version?.ToString () ?? "");
			labels.Add ("Identity:ProcessArchitecture"); values.Add (string.Join (", ", id.ProcessArchitecture));

			var prop = pkg.Properties;
			labels.Add ("Properties:DisplayName"); values.Add (prop.DisplayName);
			labels.Add ("Properties:Description"); values.Add (prop.Description);
			labels.Add ("Properties:Publisher"); values.Add (prop.Publisher);
			labels.Add ("Properties:Framework"); values.Add (prop.Framework.ToString ());
			labels.Add ("Properties:ResourcePackage"); values.Add (prop.ResourcePackage.ToString ());
			labels.Add ("Properties:Logo"); values.Add (prop.Logo);

			labels.Add ("IsBundle"); values.Add (pkg.IsBundle.ToString ());
			labels.Add ("DevelopmentMode"); values.Add (pkg.DevelopmentMode.ToString ());
			labels.Add ("InstallLocation"); values.Add (pkg.InstallLocation);
			labels.Add ("Users"); values.Add (string.Join ("; ", pkg.Users));

			Console.WriteLine ($"[{pkg.Identity.FullName}]");
			var indicesToOutput = new List<int> ();
			bool outputAll = (filter == null || !filter.Any ());

			for (int i = 0; i < labels.Count; i++)
			{
				if (outputAll)
				{
					indicesToOutput.Add (i);
				}
				else
				{
					string label = labels [i];
					foreach (string patternRaw in filter)
					{
						if (MatchPattern (label, patternRaw))
						{
							indicesToOutput.Add (i);
							break;
						}
					}
				}
			}

			if (indicesToOutput.Count == 0) return;
			int maxLabelLen = 0;
			foreach (int i in indicesToOutput)
				if (labels [i].Length > maxLabelLen) maxLabelLen = labels [i].Length;
			string format = "{0,-" + maxLabelLen + "} = {1}";
			foreach (int i in indicesToOutput)
				Console.WriteLine (format, labels [i], values [i]);
		}
		/// <summary>
		/// 匹配模式（忽略大小写，首尾空格，支持通配符 *）
		/// </summary>
		private static bool MatchPattern (string text, string pattern)
		{
			if (string.IsNullOrEmpty (pattern)) return true;
			pattern = pattern.Trim ();
			text = text ?? "";

			if (pattern == "*") return true;
			// 忽略大小写
			var comparison = StringComparison.OrdinalIgnoreCase;

			// 检查通配符位置
			bool startsWithStar = pattern.StartsWith ("*");
			bool endsWithStar = pattern.EndsWith ("*");

			if (startsWithStar && endsWithStar)
			{
				// *middle*  包含子串
				string middle = pattern.Substring (1, pattern.Length - 2);
				return text.IndexOf (middle, comparison) >= 0;
			}
			else if (startsWithStar)
			{
				// *suffix   以 suffix 结尾
				string suffix = pattern.Substring (1);
				return text.EndsWith (suffix, comparison);
			}
			else if (endsWithStar)
			{
				// prefix*   以 prefix 开头
				string prefix = pattern.Substring (0, pattern.Length - 1);
				return text.StartsWith (prefix, comparison);
			}
			else
			{
				// 精确匹配（忽略大小写）
				return string.Equals (text, pattern, comparison);
			}
		}
		/// <summary>
		/// 判断字符串是否为有效的包全名 (Package Full Name)
		/// 格式: <IdentityName>_<Version>_<ProcessorArchitecture>_<ResourceId>_<PublisherId>
		/// 其中 ResourceId 可以为空（表现为连续两个下划线）
		/// </summary>
		public static bool IsPackageFullName (string fullName)
		{
			if (string.IsNullOrWhiteSpace (fullName)) return false;
			string [] parts = fullName.Split (new char [] { '_' }, StringSplitOptions.None);
			if (parts.Length != 5)
				return false;
			if (string.IsNullOrEmpty (parts [0]))   // IdentityName
				return false;
			if (string.IsNullOrEmpty (parts [1]))   // Version
				return false;
			if (string.IsNullOrEmpty (parts [2]))   // ProcessorArchitecture
				return false;
			if (string.IsNullOrEmpty (parts [4]))   // PublisherId
				return false;
			if (!parts [1].Contains ('.')) return false;
			return true;
		}
		/// <summary>
		/// 判断字符串是否为有效的包系列名 (Package Family Name)
		/// 格式: <IdentityName>_<PublisherId>
		/// </summary>
		public static bool IsPackageFamilyName (string familyName)
		{
			if (string.IsNullOrWhiteSpace (familyName)) return false;
			string [] parts = familyName.Split ('_');
			if (parts.Length != 2) return false;
			if (string.IsNullOrEmpty (parts [0]) || string.IsNullOrEmpty (parts [1]))
				return false;
			return true;
		}
		/// <summary>
		/// 从 args[startIndex..] 生成安全的命令行字符串
		/// </summary>
		private static string BuildCommandLine (string [] args, int startIndex)
		{
			if (args.Length <= startIndex) return null;
			var sb = new StringBuilder ();
			for (int i = startIndex; i < args.Length; i++)
			{
				if (i > startIndex) sb.Append (' ');
				sb.Append (EscapeArgument (args [i]));
			}
			return sb.ToString ();
		}
		/// <summary>
		/// 按 Win32 命令行规则转义单个参数
		/// </summary>
		private static string EscapeArgument (string arg)
		{
			if (string.IsNullOrEmpty (arg)) return "\"\"";
			bool needQuotes = false;
			foreach (char c in arg)
			{
				if (char.IsWhiteSpace (c) || c == '"')
				{
					needQuotes = true;
					break;
				}
			}
			if (!needQuotes) return arg;
			var sb = new StringBuilder ();
			sb.Append ('"');
			foreach (char c in arg)
			{
				if (c == '"') sb.Append ("\\\"");
				else sb.Append (c);
			}
			sb.Append ('"');
			return sb.ToString ();
		}
		public static readonly string [] configItems = new string [] {
			"AppMetadataItems"
		};
		public static void RefreshConfig ()
		{
			var conf = new InitConfig (Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "config.ini"));
			var sSettings = conf.GetSection ("Settings");
			var kAppMetadatas = sSettings.GetKey ("PkgCLI:AppMetadataItems");
			var appMd = kAppMetadatas.ReadString ("Id,BackgroundColor,DisplayName,ForegroundText,ShortName,SmallLogo,Square44x44Logo");
			var appMdList = (appMd ?? "").Split (',', ';', '|');
			for (var i = 0; i < appMdList.Length; i ++)
			{
				appMdList [i] = appMdList [i].Trim ();
			}
			PackageReader.UpdateApplicationItems (appMdList);
		}
		static void Main (string [] args)
		{
			var currencoding = Console.OutputEncoding;
			try
			{
				//Console.OutputEncoding = Encoding.UTF8;
				if (args.Length <= 0 || args.Length >= 1 && IsHelpParam (args [0]))
				{
					PrintVersion ();
					PrintTotalHelp ();
					return;
				}
				var parser = new CliParsing ();
				parser.Params = new HashSet<CmdParamName> (new CmdParamName [] {
					new CmdParamName ("install", new string [] { "add" }),
					new CmdParamName ("register", new string [] { "reg" }),
					new CmdParamName ("update", new string [] { "up" }),
					new CmdParamName ("stage"),
					new CmdParamName ("remove", new string [] { "uninstall" }),
					new CmdParamName ("read"),
					new CmdParamName ("get"),
					new CmdParamName ("find"),
					new CmdParamName ("active", new string [] { "launch", "start", "activate" }),
					new CmdParamName ("config", new string [] { "conf" }),
					new CmdParamName ("version", new string [] { "ver" }),
					new CmdParamName ("help", new string [] { "?", "h" }),
					new CmdParamName ("developmode", new string [] { "develop" }),
					new CmdParamName ("forceappshutdown", new string [] { "forceshutdown" , "appshutdown", "forcesd", "force"}),
					new CmdParamName ("installallresources", new string [] {"allresources", "allres"}),
					new CmdParamName ("savexml"),
					new CmdParamName ("savejson"),
					new CmdParamName ("fullname"),
					new CmdParamName ("filter"),
					new CmdParamName ("package", new string [] { "pkg" }),
					new CmdParamName ("manifest", new string [] { "mani" }),
					new CmdParamName ("usepri", new string [] { "pri" }),
					new CmdParamName ("item"),
					new CmdParamName ("set"),
					new CmdParamName ("refresh"),
					new CmdParamName ("show"),
					new CmdParamName ("encoding", new string [] { "en", "charset", "encode" }),
					new CmdParamName ("yes", new string [] {"y", "agree"})
				});
				RefreshConfig ();
				var cmds = parser.Parse (args);
				if (cmds.ParamContains ("encoding"))
				{
					#region help text: encoding
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /encoding:<code_page|name>
    pkgcli /en:<code_page|name>

Operation:
    Set the console output encoding for the current session.
    Useful when the output contains non-ASCII characters.

Arguments:
    <code_page>     Numeric code page identifier (e.g., 65001 for UTF-8, 936 for GB2312).
    <name>          Encoding name (e.g., utf-8, gb2312, windows-1252).

Examples:
    pkgcli /encoding:65001      (Set to UTF-8)
    pkgcli /encoding:utf-8
    pkgcli /en:936              (Set to GB2312/GBK)
    pkgcli /en:gb2312

Note:
    This setting only affects the current pkgcli process and does not persist.
");
						return;
					}
					#endregion
					try
					{
						var c = cmds.GetFromId ("encoding");
						var i = 0;
						var isint = false;
						Encoding en = null;
						try { i = Convert.ToInt32 (c.Value); isint = true; } catch { isint = false; }
						if (isint) en = Encoding.GetEncoding (i);
						else en = Encoding.GetEncoding (c.Value);
						Console.OutputEncoding = en;
					}
					catch (Exception ex)
					{
						Console.ForegroundColor = ConsoleColor.Yellow;
						Console.WriteLine ($"Warning: Set encoding for output failed. Exception {ex.GetType ()}\nMessage: \n    {ex.Message}\nWe will use default encoding.");
						Console.ResetColor ();
					}
				}
				if (CliPasingUtils.ParamsContainsOr (cmds, "install", "register", "update", "stage"))
				{
					#region help text: install register, update, stage
					PrintVersion ();
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						Console.WriteLine (@"
Usage:
    pkgcli <command> <file_path...> [/developmode] [/force] [/allres]

Commands:
    /install        Install a package. Supports .appx, .appxbundle, .msix, .msixbundle.
    /register       Register a package. Supports AppxManifest.xml or *.appxmanifest.
    /update         Update an installed app. Supports same formats as /install.
    /stage          Stage a package (pre-deploy). Supports same formats as /install.

Options (DeploymentOptions flags):
    /developmode    Install/register in development mode. Do not use with bundle packages.
    /force          Force application shutdown to allow registration when the package or its dependencies are in use.
    /allres         Skip resource applicability checks; stage/register all resource packages in a bundle.

Arguments:
    <file_path...>  One or more package or manifest file paths. Can be:
                    - Direct path to .appx, .msix, .appxbundle, .msixbundle, or .xml manifest.
                    - A .txt file containing a list of such paths (one per line).
                    - If no command is repeated, the command applies to all listed files.

Examples:
    pkgcli /install MyApp.appx
    pkgcli /register /manifest:AppxManifest.xml /developmode
    pkgcli /update MyApp.msixbundle /force
    pkgcli /stage MyApp.appx /allres
");
						return;
					}
					#endregion
					var options = DeploymentOptions.None;
					if (CliPasingUtils.ParamContains (cmds, "developmode")) options |= DeploymentOptions.DevelopmentMode;
					if (CliPasingUtils.ParamContains (cmds, "forceappshutdown")) options |= DeploymentOptions.ForceAppShutdown;
					if (CliPasingUtils.ParamContains (cmds, "installallresources")) options |= DeploymentOptions.InstallAllResources;
					var totallist = new List<Tuple<int, string>> ();
					var filelist = new List<string> ();
					foreach (var f in cmds)
					{
						if (string.IsNullOrWhiteSpace (f.Id))
						{
							var file = f.Value;
							AddFiles (filelist, file);
						}
						else
						{
							switch (f.Id)
							{
								case "install":
								case "register":
								case "update":
								case "stage":
									AddFiles (filelist, f.Value);
									break;
							}
						}
					}
					foreach (var f in filelist) totallist.Add (new Tuple<int, string> (0, f));
					if (cmds.ParamContains ("register") && cmds.ParamContains ("fullname"))
					{
						foreach (var f in cmds)
						{
							if (f.Id.NEquals ("fullname"))
							{
								if (string.IsNullOrWhiteSpace (f.Value))
								{
									totallist.Add (new Tuple<int, string> (1, f.Value));
								}
							}
						}

					}
					PackageOperation ope = null;
					if (cmds.ParamContains ("install")) ope = AddPackage;
					else if (cmds.ParamContains ("register")) ope = RegisterPackage;
					else if (cmds.ParamContains ("update")) ope = UpdatePackage;
					else if (cmds.ParamContains ("stage")) ope = StagePackage;
					for (int i = 0; i < totallist.Count; i++)
					{
						Console.WriteLine ();
						var file = totallist [i];
						Console.Write ($"\r({i + 1}/{totallist.Count}) Operation in progress...");
						var hr = new _I_HResult (0);
						var tempope = ope;
						if (file.Item1 == 1)
						{
							if (cmds.ParamContains ("register")) tempope = RegisterPackageByFullName;
						}
						hr = tempope (file.Item2, null, options, prog => {
							var str = $"\r({i + 1}/{filelist.Count}) Operation in progress... {prog}% of {(int)((i + prog * 0.01) / filelist.Count * 100)}%";
							Console.Write (str);
						});
						if (hr.Failed)
						{
							Console.ForegroundColor = ConsoleColor.Red;
							Console.Write ($"\nPackage {i + 1} \"{file.Item2}\" Operation Error ({"0x" + hr.HResult.ToString ("X8")}): \n{hr.Message}");
							Console.ResetColor ();
						}
					}
					Console.WriteLine ("\nAll Done.");
					return;
				}
				else if (cmds.ParamsContainsOr ("remove"))
				{ 
					#region help text: remove
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /remove <package_full_name> [<package_full_name>...] [/yes]

Operation:
    Remove one or more packages.

Arguments:
    <package_full_name>  The full name of the installed package.
                         Format: <identity_name>_<version>_<architecture>_<resource_id>_<publisher_id>
                         Example: Microsoft.WinJS.1.0_1.0.9200.20789_neutral__8wekyb3d8bbwe

Options:
    /yes                Automatically confirm the uninstallation without prompting.
                        Aliases: /y, /agree

You can specify multiple full names separated by spaces.

Examples:
    pkgcli /remove MyPackage_1.0.0.0_x64__abcd1234
    pkgcli /remove PackageA_1.0.0.0_neutral__abcd1234 PackageB_2.0.0.0_x86__efgh5678 /yes
");
						return;
					}
					#endregion
					var list = new List<string> ();
					foreach (var c in cmds)
					{
						if (string.IsNullOrWhiteSpace (c.Id) || c.Id.NEquals ("remove"))
						{
							if (!string.IsNullOrWhiteSpace (c.Value)) list.Add (c.Value);
						}
					}
					var agreecmd = cmds.GetFromId ("yes");
					bool? agree = null;
					if (agreecmd != null) agree = true;
					if (agree == null)
					{
						if (list.Count <= 0) agree = true;
						else
						{
							Console.Write ($"We will uninstall {list.Count} app(-s). Do you want to continue?(Y/N) ");
							var userinput = Console.ReadLine ();
							if (userinput.NEquals ("y") || userinput.NEquals ("yes")) agree = true;
							else agree = false;
						}
					}
					if (agree == false) throw new OperationCanceledException ("User canceled.");
					for (int i = 0; i < list.Count; i++)
					{
						Console.WriteLine ();
						var file = list [i];
						Console.Write ($"\r({i + 1}/{list.Count}) Operation in progress...");
						var hr = RemovePackage (file, prog => {
							var str = $"\r({i + 1}/{list.Count}) Operation in progress... {prog}% of {(int)((i + prog * 0.01) / list.Count * 100)}%";
							Console.Write (str);
						});
						if (hr.Failed)
						{
							Console.ForegroundColor = ConsoleColor.Red;
							Console.Write ($"\nPackage {i + 1} \"{file}\" Operation Error ({"0x" + hr.HResult.ToString ("X8")}): \n{hr.Message}");
							Console.ResetColor ();
						}
					}
					Console.WriteLine ("\nAll Done.");
					return;
				}
				else if (cmds.ParamsContainsOr ("get"))
				{
					#region help text: get
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /get [/filter:<filter1,filter2,...>]

Operation:
    List all installed packages with their properties.

Options:
    /filter:<filters>   Show only the specified properties.
                        Filters are case-insensitive and support '*' wildcard.
                        Multiple filters separated by comma ',' or semicolon ';'.
                        Examples:
                            /filter:Identity:Name,Identity:Version
                            /filter:Identity:*                 (all Identity properties)
                            /filter:Properties:DisplayName

Output:
    Each package is printed as a section [FullName] followed by key = value lines.
    Redirect output to a file to save as INI-like format.

Examples:
    pkgcli /get
    pkgcli /get /filter:Identity:FullName,Properties:DisplayName
");
						return;
					}
					#endregion
					var filter = new HashSet<string> (new NormalizeStringComparer ());
					if (cmds.ParamContains ("filter"))
					{
						foreach (var c in cmds)
						{
							if (c.Id.NEmpty () || c.Id.NEquals ("filter"))
							{
								var items = c.Value.Split (',', ';');
								foreach (var i in items) filter.Add (i);
							}
						}
					}
					var hr = GetPackages ();
					if (hr.Item1.Failed)
					{
						Console.ForegroundColor = ConsoleColor.Red;
						Console.WriteLine ($"Operation Error ({"0x" + hr.Item1.HResult.ToString ("X8")}): \n{hr.Item1.Message}");
						Console.ResetColor ();
						return;
					}
					foreach (var i in hr.Item2)
					{
						ToFormatString (i, filter);
						Console.WriteLine ();
					}
					Console.WriteLine ("[Statistic]");
					Console.WriteLine ($"Length = {hr.Item2.Count}");
					return;
				}
				else if (cmds.ParamsContainsOr ("find"))
				{
					#region help text: find
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
Console.WriteLine(@"
Usage:
    pkgcli /find <package_full_name|package_family_name> [/filter:<filters>]
    pkgcli /find <identity_name> <identity_publisher> [/filter:<filters>]

Operation:
    Find installed packages matching the given identifier.

Arguments:
    <package_full_name>       Full name of a package.
    <package_family_name>     Family name of a package.
    <identity_name>           Name part of the package identity (e.g., ""Microsoft.WindowsStore"").
    <identity_publisher>      Publisher part (e.g., ""CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"").

Options:
    /filter:<filters>         Same as in /get command.

Examples:
    pkgcli /find Microsoft.WindowsStore_8wekyb3d8bbwe!App
    pkgcli /find Microsoft.WindowsStore
    pkgcli /find Microsoft.WindowsStore ""CN=Microsoft Corporation, ...""
");
						return;
					}
					#endregion
					var names = new List<string> ();
					foreach (var c in cmds)
					{
						if (c.Id.NEmpty ())
						{
							if (c.Value.NEmpty ()) continue;
							names.Add (c.Value);
						}
					}
					var filter = new HashSet<string> (new NormalizeStringComparer ());
					if (cmds.ParamContains ("filter"))
					{
						foreach (var c in cmds)
						{
							if (c.Id.NEquals ("filter"))
							{
								var items = c.Value.Split (',', ';');
								foreach (var i in items) filter.Add (i);
							}
						}
					}
					var result = new List<Tuple<string, Tuple<_I_HResult, List<PMPackageInfo>>>> ();
					if (names.Count == 2)
					{
						result.Add (new Tuple<string, Tuple<_I_HResult, List<PMPackageInfo>>> (names [0], FindPackage (names [0], names [1])));
					}
					else
					{
						foreach (var n in names)
						{
							if (IsPackageFullName (n))
							{
								result.Add (new Tuple<string, Tuple<_I_HResult, List<PMPackageInfo>>> (n, FindPackageByFullName (n)));
							}
							else
							{
								result.Add (new Tuple<string, Tuple<_I_HResult, List<PMPackageInfo>>> (n, FindPackage (n)));
							}
						}
					}
					foreach (var r in result)
					{
						foreach (var l in r.Item2.Item2)
						{
							ToFormatString (l, filter);
							Console.WriteLine ();
						}
						Console.WriteLine ($"[Statistic:{r.Item1}]");
						Console.WriteLine ($"HResult = {"0x" + r.Item2.Item1.HResult.ToString ("X8")}");
						Console.WriteLine ($"Message = {Escape.ToEscape (r.Item2.Item1.Message)}");
						Console.WriteLine ($"Length = {r.Item2.Item2.Count}");
					}
					return;
				}
				else if (cmds.ParamsContainsOr ("active"))
				{
					#region help text: active
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /active <app_user_model_id> [arguments]

Operation:
    Launch (activate) a Universal Windows Platform (UWP) app.

Arguments:
    <app_user_model_id>       The Application User Model ID (AUMID).
                              Format: <package_family_name>!<app_id>
                              Example: Microsoft.WindowsStore_8wekyb3d8bbwe!App

    [arguments]               Optional command-line arguments passed to the app.

Examples:
    pkgcli /active Microsoft.WindowsStore_8wekyb3d8bbwe!App
    pkgcli /active MyAppFamily!App --fullscreen --debug
");
						return;
					}
					#endregion
					foreach (var c in cmds)
					{
						if (string.IsNullOrWhiteSpace (c.Id) || c.Id.NEquals ("active"))
						{
							if (c.Value.NEmpty ()) continue;
							var i = 0;
							for (i = 0; i < args.Length; i++)
							{
								if (args [i].NNormalize ().IndexOf (c.Value.NNormalize ()) >= 0) break;
							}
							var hr = ActiveApp (c.Value, BuildCommandLine (args, i + 1));
							if (hr.Succeeded) Console.WriteLine ("Done.");
							else
							{
								Console.ForegroundColor = ConsoleColor.Red;
								Console.Write ($"Operation Error ({"0x" + hr.HResult.ToString ("X8")}): \n{hr.Message}");
								Console.ResetColor ();
							}
							return;
						}
					}
					return;
				}
				else if (cmds.ParamsContainsOr ("read"))
				{
					#region help text: read
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /read [options] [<file>]

Description:
    Read an Appx/MSIX package (.appx, .appxbundle, .msix, .msixbundle) or its manifest file (.xml, .appxpackage, .msixpackage).
    Extracts package/manifest information and outputs as JSON to the console, or saves as a ZIP archive containing either info.json or info.xml.

Options:
    /manifest:<file>        Specify a manifest file to read.
    /package:<file>         Specify a package file to read.
    <file>                  If no /manifest or /package is provided, the first unnamed parameter is used as the input file.
                            File type is auto-detected by extension:
                              .xml, .appxpackage, .msixpackage  → ManifestReader
                              .appx, .appxbundle, .msix, .msixbundle → PackageReader

    /item:<path>            Instead of outputting the whole JSON object, output only the value at the given path.
                            Path syntax: use '.' or ':' as separators, case-insensitive.
                            Supports indexers (e.g., [0]), .length for collections, and automatic Base64 access (e.g., LogoBase64).
                            Examples:
                                /item:Identity.Name
                                /item:Properties.Publisher
                                /item:applications[0]
                                /item:applications.length
                                /item:applications[0].LogoBase64

    /usepri                 Enable PRI resource resolution (localization and scaled images). Required for proper resource string and image resolution.

    /savexml:<output>       Save extracted data as an XML file wrapped in a ZIP archive.
                            The archive will contain an info.xml file with the structured data.

    /savejson:<output>      Save extracted data as a JSON file wrapped in a ZIP archive.
                            The archive will contain an info.json file with the structured data.

    /help                   Display this help text.

Output Behavior:
    - If neither /savexml nor /savejson is given, the information is printed directly to the console as JSON.
    - If /item is used, only the value at the specified path is printed (as a string, version, or formatted JSON).
    - If /savexml or /savejson is used, the data is saved to the specified file (ZIP archive) and a success/failure message is shown.

Examples:
    pkgcli /read MyApp.appx
    pkgcli /read /manifest:AppxManifest.xml /usepri
    pkgcli /read /package:MyApp.msixbundle /item:Identity.Name
    pkgcli /read /package:MyApp.appx /savejson:output.zip
    pkgcli /read MyApp.appx /item:applications[0].DisplayName
");
						return;
					}
					#endregion
					var filename = "";
					var readtype = "";
					var savename = "";
					var savetype = "default";
					CommandParam cmd = null;
					cmd = cmds.GetFromId ("manifest");
					if (cmd != null)
					{
						filename = cmd.Value;
						readtype = "manifest";
					}
					cmd = cmds.GetFromId ("package");
					if (cmd != null)
					{
						filename = cmd.Value;
						readtype = "package";
					}
					if (string.IsNullOrWhiteSpace (filename) || !File.Exists (filename))
					{
						cmd = cmds.GetFromId ("");
						if (cmd != null)
						{
							if (File.Exists (cmd.Value))
							{
								filename = cmd.Value;
								if (string.IsNullOrWhiteSpace (readtype))
								{
									var ext = Path.GetExtension (cmd.Value);
									if (ext.NEquals (".xml") || ext.NEquals (".appxpackage") || ext.NEquals (".msixpackage"))
										readtype = "manifest";
									else readtype = "package";
								}
							}
						}
					}
					if (string.IsNullOrWhiteSpace (filename) || !File.Exists (filename))
					{
						cmd = cmds.GetFromId ("read");
						if (cmd != null)
						{
							if (File.Exists (cmd.Value))
							{
								filename = cmd.Value;
								if (string.IsNullOrWhiteSpace (readtype))
								{
									var ext = Path.GetExtension (cmd.Value);
									if (ext.NEquals (".xml") || ext.NEquals (".appxpackage") || ext.NEquals (".msixpackage"))
										readtype = "manifest";
									else readtype = "package";
								}
							}
						}
					}
					if (string.IsNullOrWhiteSpace (filename) || !File.Exists (filename)) throw new FileNotFoundException ();
					cmd = cmds.GetFromId ("savexml");
					if (cmd != null)
					{
						savename = cmd.Value;
						savetype = "xml";
					}
					if (string.IsNullOrWhiteSpace (savename))
					{
						cmd = cmds.GetFromId ("savejson");
						if (cmd != null)
						{
							savename = cmd.Value;
							savetype = "json";
						}
					}
					if (string.IsNullOrWhiteSpace (savename))
					{
						savetype = "ini";
					}
					switch (readtype)
					{
						default:
						case "package":
							{
								var pr = new PackageReader (filename);
								pr.UsePri = cmds.ParamContains ("usepri");
								pr.EnablePri = true;
								switch (savetype)
								{
									default:
									case "default":
										{
											if (cmds.ParamContains ("item"))
											{
												object value = pr.GetItem (cmds.GetFromId ("item").Value);
												if (value is string) Console.WriteLine (value as string);
												else if (value is DataUtils.Version) Console.WriteLine (value.ToString ());
												else Console.WriteLine (JsonConvert.SerializeObject (value, Formatting.Indented));
											}
											else Console.WriteLine (JsonConvert.SerializeObject (pr.GetJsonObjectForCli (), Formatting.Indented));
										}
										break;
									case "json":
										{
											var res = pr.SaveJsonFileCS (savename);
											if (res) Console.WriteLine ("Succeeded!");
											else Console.WriteLine ("Failed.");
										}
										break;
									case "xml":
										{
											var res = pr.SaveXmlFileCS (savename);
											if (res) Console.WriteLine ("Succeeded!");
											else Console.WriteLine ("Failed.");
										}
										break;
								}
							}
							break;
						case "manifest":
							{
								var mr = new ManifestReader (filename);
								mr.UsePri = cmds.ParamContains ("usepri");
								mr.EnablePri = true;
								if (cmds.ParamContains ("item"))
								{
									object value = mr.GetItem (cmds.GetFromId ("item").Value);
									if (value is string) Console.WriteLine (value as string);
									else if (value is DataUtils.Version) Console.WriteLine (value.ToString ());
									else Console.WriteLine (JsonConvert.SerializeObject (value, Formatting.Indented));
								}
								else Console.WriteLine (JsonConvert.SerializeObject (mr.GetJsonObjectForCli (), Formatting.Indented));
								//Console.WriteLine (mr.BuildJsonText ());
							}
							break;
					}
					return;
				}
				else if (cmds.ParamsContainsOr ("config"))
				{
					#region help text: config
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /config                      Show current configuration (all keys).
    pkgcli /config /show:<key>          Show value of a specific configuration key.
    pkgcli /config /set:<key> <value>   Set configuration key to the given value.
    pkgcli /config /refresh             Reload configuration from config.ini.

Configuration keys:
    AppMetadataItems    Comma-separated list of application metadata fields to read from manifests.
                        Default: ""Id,BackgroundColor,DisplayName,ForegroundText,ShortName,SmallLogo,Square44x44Logo""

Configuration file:
    config.ini in the same directory as pkgcli.exe.

Examples:
    pkgcli /config
    pkgcli /config /show:AppMetadataItems
    pkgcli /config /set:AppMetadataItems ""Id,DisplayName,Logo""
    pkgcli /config /refresh
");
						return;
					}
					#endregion
					var conf = new InitConfig (Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "config.ini"));
					var sSettings = conf.GetSection ("Settings");
					if (cmds.ParamContains ("refresh")) RefreshConfig ();
					else if (cmds.ParamContains ("set"))
					{
						var cmd = cmds.GetFromId ("set");
						var key = cmd.Value;
						if (string.IsNullOrWhiteSpace (key)) throw new InvalidOperationException ($"key is empty");
						var isfind = false;
						foreach (var i in configItems)
						{
							if (i.NEquals (key))
							{
								isfind = true;
								break;
							}
						}
						if (!isfind) throw new KeyNotFoundException ($"Error: cannot find the key \"{key}\"");
						var valuelist = new List<string> ();
						foreach (var c in cmds)
						{
							if (c.Id.NEmpty ())
							{
								if (string.IsNullOrWhiteSpace (c.Value)) continue;
								valuelist.Add (c.Value);
							}
						}
						var res = sSettings.GetKey ($"PkgCLI:{key.Trim ()}").Set (valuelist.Join (","));
						if (res) Console.WriteLine ("Succeeded!");
						else Console.WriteLine ("Failed.");
					}
					else if (cmds.ParamContains ("show"))
					{
						var cmd = cmds.GetFromId ("show");
						var key = cmd.Value;
						if (string.IsNullOrWhiteSpace (key))
						{
							var dict = new Dictionary<string, string> ();
							foreach (var k in configItems)
							{
								var cKey = sSettings.GetKey ($"PkgCLI:{k}");
								dict [k] = cKey.ReadString ("(use default)");
							}
							Console.WriteLine (dict.FormatDictionaryAligned ("="));
							return;
						}
						var isfind = false;
						foreach (var i in configItems)
						{
							if (i.NEquals (key))
							{
								isfind = true;
								break;
							}
						}
						if (!isfind) throw new KeyNotFoundException ($"Error: cannot find the key \"{key}\"");
						var value = sSettings.GetKey ($"PkgCLI:{key.Trim ()}").ReadString ("(use default)");
						Console.WriteLine (value);
					}
					return;
				}
				else if (cmds.ParamsContainsOr ("version"))
				{
					#region help text: version
					if (CliPasingUtils.ParamContains (cmds, "help"))
					{
						PrintVersion ();
						Console.WriteLine (@"
Usage:
    pkgcli /version
    pkgcli /ver

Operation:
    Display the current version of Package Manager CLI.

Examples:
    pkgcli /version
    pkgcli /ver
");
						return;
					}
					#endregion
					PrintVersion ();
					return;
				}
				else
				{
					Console.ForegroundColor = ConsoleColor.Red;
					Console.WriteLine ("Invalid args. Please use \"/help\" to get help.");
					Console.ResetColor ();
				}
			}
			catch (Exception ex)
			{
				Console.ForegroundColor = ConsoleColor.Red;
				Console.WriteLine ($"Exception {ex.GetType ()}, \nMessage: \n    {ex.Message}\nStack: \n    {ex.StackTrace}");
			}
			finally
			{
				Console.ResetColor ();
				Console.OutputEncoding = currencoding;
			}
		}
	}
}
