using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace FileLiberator;

public interface IProcessable<T> where T : IProcessable<T>
{
	/// <summary>
	/// Create a new instance of the Processable which uses a specific Configuration
	/// </summary>
	/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
	static abstract T Create(Configuration config);
}
public abstract class Processable
{
	public abstract string Name { get; }
	public event EventHandler<LibraryBook>? Begin;

	/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
	public event EventHandler<string>? StatusUpdate;
	/// <summary>Fired when a file is successfully saved to disk</summary>
	public event EventHandler<(string id, string path)>? FileCreated;
	public event EventHandler<DownloadProgress>? StreamingProgressChanged;
	public event EventHandler<TimeSpan>? StreamingTimeRemaining;

	public event EventHandler<LibraryBook>? Completed;

	public required Configuration Configuration { get; init; }
	protected Processable() { }

	/// <returns>True == Valid</returns>
	public abstract bool Validate(LibraryBook libraryBook);

	/// <returns>True == success</returns>
	public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);

	// when used in foreach: stateful. deferred execution
	public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
		=> library.Where(libraryBook =>
			Validate(libraryBook)
			&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
			);

	public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
	{
		if (validate && !Validate(libraryBook))
			return new StatusHandler { "Validation failed" };

		Serilog.Log.Logger.Information("Begin " + nameof(ProcessSingleAsync) + " {@DebugInfo}", new
		{
			libraryBook.Book.TitleWithSubtitle,
			libraryBook.Book.AudibleProductId,
			libraryBook.Book.Locale,
			Account = libraryBook.Account?.ToMask() ?? "[empty]"
		});

		var status
			= (await ProcessAsync(libraryBook))
			?? new StatusHandler { "Processable should never return a null status" };

		GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);

		return status;
	}

	public async Task<StatusHandler> TryProcessAsync(LibraryBook libraryBook)
		=> Validate(libraryBook)
		? await ProcessAsync(libraryBook)
		: new StatusHandler();

	protected void OnBegin(LibraryBook libraryBook)
	{
		Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Begin), Book = libraryBook.LogFriendly() });
		Begin?.Invoke(this, libraryBook);
	}

	protected void OnStatusUpdate(string statusUpdate)
	{
		Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(StatusUpdate), Status = statusUpdate });
		StatusUpdate?.Invoke(this, statusUpdate);
	}

	protected void OnFileCreated(LibraryBook libraryBook, string path)
	{
		Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), libraryBook.Book.AudibleProductId, path });
		FilePathCache.Insert(libraryBook.Book.AudibleProductId, path);
		FileCreated?.Invoke(this, (libraryBook.Book.AudibleProductId, path));
	}

	protected void OnStreamingProgressChanged(DownloadProgress progress)
		=> OnStreamingProgressChanged(null, progress);
	protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
		=> StreamingProgressChanged?.Invoke(this, progress);

	protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
		=> OnStreamingTimeRemaining(null, timeRemaining);
	protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
		=> StreamingTimeRemaining?.Invoke(this, timeRemaining);

	protected void OnCompleted(LibraryBook libraryBook)
	{
		Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(Completed), Book = libraryBook.LogFriendly() });
		Completed?.Invoke(this, libraryBook);
	}

	protected void SetFileTime(LibraryBook libraryBook, string file)
		=> setFileSystemTime(libraryBook, new FileInfo(file));
	protected void SetDirectoryTime(LibraryBook libraryBook, string file)
		=> setFileSystemTime(libraryBook, new DirectoryInfo(file));

	private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
	{
		if (!fileInfo.Exists) return;

		DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
		{
			Configuration.DateTimeSource.Added => libraryBook.DateAdded,
			Configuration.DateTimeSource.Published => libraryBook.Book.DatePublished,
			_ => null,
		};

		if (getTimeValue(Configuration.CreationTime) is { } creationUtc)
		{
			try
			{
				fileInfo.CreationTimeUtc = creationUtc;
			}
			catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
			{
				Serilog.Log.Logger.Debug(ex, "Could not set creation time for {Path}; filesystem may not support it.", fileInfo.FullName);
			}
		}

		if (getTimeValue(Configuration.LastWriteTime) is { } lastWriteUtc)
		{
			try
			{
				fileInfo.LastWriteTimeUtc = lastWriteUtc;
			}
			catch (Exception ex) when (ex is UnauthorizedAccessException or IOException)
			{
				Serilog.Log.Logger.Debug(ex, "Could not set last write time for {Path}; filesystem may not support it.", fileInfo.FullName);
			}
		}
	}
}
