using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dinah.Core;
using Polly;
using Polly.Retry;
using Dinah.Core.Collections.Generic;

namespace FileManager
{
	public static class FileUtility
	{


		/// <summary>
		/// "txt" => ".txt"
		/// <br />".txt" => ".txt"
		/// <br />null or whitespace => ""
		/// </summary>
		public static string GetStandardizedExtension(string extension)
			=> string.IsNullOrWhiteSpace(extension)
			? (extension ?? "")?.Trim()
			: '.' + extension.Trim().Trim('.');

		/// <summary>
		/// Return position with correct number of leading zeros.
		/// <br />- 2 of 9 => "2"
		/// <br />- 2 of 90 => "02"
		/// <br />- 2 of 900 => "002"
		/// </summary>
		/// <param name="position">position in sequence. The 'x' in 'x of y'</param>
		/// <param name="total">total qty in sequence. The 'y' in 'x of y'</param>
		public static string GetSequenceFormatted(int position, int total)
		{
			ArgumentValidator.EnsureGreaterThan(position, nameof(position), 0);
			ArgumentValidator.EnsureGreaterThan(total, nameof(total), 0);
			if (position > total)
				throw new ArgumentException($"{position} may not be greater than {total}");

			return position.ToString().PadLeft(total.ToString().Length, '0');
		}


		/// <summary>
		/// Ensure valid file name path: 
		/// <br/>- remove invalid chars
		/// <br/>- ensure uniqueness
		/// <br/>- enforce max file length
		/// </summary>
		public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
		{
			ArgumentValidator.EnsureNotNull(path, nameof(path));
			ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
			fileExtension = GetStandardizedExtension(fileExtension);

			// remove invalid chars
			path = GetSafePath(path, replacements);

			// ensure uniqueness and check lengths
			var dir = Path.GetDirectoryName(path);
			dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;

			var fileName = Path.GetFileName(path);
			var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
			var filenameWithoutExtension = extIndex >= 0 ? fileName.Remove(extIndex, fileExtension.Length) : fileName;
			var fileStem
				= Path.Combine(dir, filenameWithoutExtension.TruncateFilename(LongPath.MaxFilenameLength - fileExtension.Length))
				.TruncateFilename(LongPath.MaxPathLength - fileExtension.Length);

			var fullfilename = removeInvalidWhitespace(fileStem) + fileExtension;

			var i = 0;
			while (File.Exists(fullfilename) && !returnFirstExisting)
			{
				var increm = $" ({++i})";
				fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - increm.Length - fileExtension.Length) + increm + fileExtension;
			}

			return fullfilename;
		}

		/// <summary>Use with full path, not file name. Valid path characters which are invalid file name characters will be retained: '\\', '/'</summary>
		public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
		{
			ArgumentValidator.EnsureNotNull(path, nameof(path));

			var pathNoPrefix = path.PathWithoutPrefix;

			pathNoPrefix = replacements.ReplacePathChars(pathNoPrefix);
			pathNoPrefix = removeDoubleSlashes(pathNoPrefix);

			return pathNoPrefix;
		}

		private static string removeDoubleSlashes(string path)
		{
			if (path.Length < 2)
				return path;

			// exception: don't try to condense the initial dbl bk slashes in a path. eg: \\192.168.0.1

			var remainder = path[1..];
			var dblSeparator = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
			while (remainder.Contains(dblSeparator))
				remainder = remainder.Replace(dblSeparator, $"{Path.DirectorySeparatorChar}");

			return path[0] + remainder;
		}

		private static string removeInvalidWhitespace_pattern { get; } = $@"[\s\.]*\{Path.DirectorySeparatorChar}\s*";
		private static Regex removeInvalidWhitespace_regex { get; } = new(removeInvalidWhitespace_pattern, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);

		/// <summary>no part of the path may begin or end in whitespace</summary>
		private static string removeInvalidWhitespace(string fullfilename)
		{
			// no whitespace at beginning or end
			// replace whitespace around path slashes
			//    regex (with space added for clarity)
			//    \s*  \\  \s*    =>    \
			// no ending dots. beginning dots are valid

			// regex is easier by ending with separator
			fullfilename += Path.DirectorySeparatorChar;
			fullfilename = removeInvalidWhitespace_regex.Replace(fullfilename, Path.DirectorySeparatorChar.ToString());
			// take separator back off
			fullfilename = RemoveLastCharacter(fullfilename);

			fullfilename = removeDoubleSlashes(fullfilename);
			return fullfilename;
		}

		public static string RemoveLastCharacter(this string str) => string.IsNullOrEmpty(str) ? str : str[..^1];

		public static string TruncateFilename(this string filenameStr, int limit)
		{
			if (LongPath.IsWindows) return filenameStr.Truncate(limit);

			int index = filenameStr.Length;

			while (index > 0 && System.Text.Encoding.UTF8.GetByteCount(filenameStr, 0, index) > limit)
				index--;

			return filenameStr[..index];
		}

		/// <summary>
		/// Move file.
		/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
		/// <br/>- Perform <see cref="SaferMove"/>
		/// <br/>- Return valid path
		/// </summary>
		public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements, string extension = null)
		{
			extension = extension ?? Path.GetExtension(source);
			destination = GetValidFilename(destination, replacements, extension);
			SaferMove(source, destination);
			return destination;
		}

		private static int maxRetryAttempts { get; } = 3;
		private static TimeSpan pauseBetweenFailures { get; } = TimeSpan.FromMilliseconds(100);
		private static RetryPolicy retryPolicy { get; } =
			Policy
			.Handle<Exception>()
			.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);

		/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
		public static void SaferDelete(LongPath source)
			=> retryPolicy.Execute(() =>
			{
				try
				{
					if (!File.Exists(source))
					{
						Serilog.Log.Logger.Debug("No file to delete: {@DebugText}", new { source });
						return;
					}

					Serilog.Log.Logger.Debug("Attempt to delete file: {@DebugText}", new { source });
					File.Delete(source);
					Serilog.Log.Logger.Information("File successfully deleted: {@DebugText}", new { source });
				}
				catch (Exception e)
				{
					Serilog.Log.Logger.Error(e, "Failed to delete file: {@DebugText}", new { source });
					throw;
				}
			});

		/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
		public static void SaferMove(LongPath source, LongPath destination)
			=> retryPolicy.Execute(() =>
			{
				try
				{
					if (!File.Exists(source))
					{
						Serilog.Log.Logger.Debug("No file to move: {@DebugText}", new { source });
						return;
					}

					SaferDelete(destination);

					var dir = Path.GetDirectoryName(destination);
					Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
					Directory.CreateDirectory(dir);

					Serilog.Log.Logger.Debug("Attempt to move file: {@DebugText}", new { source, destination });
					File.Move(source, destination);
					Serilog.Log.Logger.Information("File successfully moved: {@DebugText}", new { source, destination });
				}
				catch (Exception e)
				{
					Serilog.Log.Logger.Error(e, "Failed to move file: {@DebugText}", new { source, destination });
					throw;
				}
			});

		/// <summary>
		/// A safer way to get all the files in a directory and sub directory without crashing on UnauthorizedException or PathTooLongException
		/// </summary>
		/// <param name="rootPath">Starting directory</param>
		/// <param name="patternMatch">Filename pattern match</param>
		/// <param name="searchOption">Search subdirectories or only top level directory for files</param>
		/// <returns>List of files</returns>
		public static IEnumerable<LongPath> SaferEnumerateFiles(LongPath path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
		{
			var foundFiles = Enumerable.Empty<LongPath>();

			try
			{
				if (searchOption == SearchOption.AllDirectories)
				{
					IEnumerable<LongPath> subDirs = Directory.EnumerateDirectories(path).Select(p => (LongPath)p);
					// Add files in subdirectories recursively to the list
					foreach (string dir in subDirs)
						foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
				}

				// Add files from the current directory
				foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern).Select(f => (LongPath)f));
			}
			catch (UnauthorizedAccessException) { }
			catch (PathTooLongException) { }
            // Symbolic links will result in DirectoryNotFoundException. Ohter logical directories might also. Just skip them. Don't want to risk (or have to handle) infinite recursion
            catch (DirectoryNotFoundException) { }

			return foundFiles;
		}
	}
}
