using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.Manuals;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using Microsoft.Extensions.Configuration;

namespace AsbCloudInfrastructure.Services;

public class ManualCatalogService : IManualCatalogService
{
	private const int IdFileCategory = 30000;

	private readonly IEnumerable<string> validExtensions = new[]
	{
		".pdf",
		".mp4"
	};

	private readonly string directoryFiles;
	private readonly IFileStorageRepository fileStorageRepository;
	private readonly IManualDirectoryRepository manualDirectoryRepository;
	private readonly ICrudRepository<ManualDto> manualRepository;

	public ManualCatalogService(IFileStorageRepository fileStorageRepository,
		IManualDirectoryRepository manualDirectoryRepository,
		ICrudRepository<ManualDto> manualRepository,
		IConfiguration configuration)
	{
		this.fileStorageRepository = fileStorageRepository;
		this.manualDirectoryRepository = manualDirectoryRepository;
		this.manualRepository = manualRepository;
		directoryFiles = configuration.GetValue<string>("DirectoryManualFiles");

		if (string.IsNullOrWhiteSpace(directoryFiles))
			directoryFiles = "manuals";
	}

	public async Task<int> SaveFileAsync(int idDirectory, int idAuthor, string name, Stream stream, CancellationToken cancellationToken)
	{
		var extension = Path.GetExtension(name);

		if (!validExtensions.Contains(extension))
			throw new ArgumentInvalidException(
                nameof(name),
                $"Невозможно загрузить файл с расширением '{extension}'. Допустимые форматы файлов: {string.Join(", ", validExtensions)}");

		var path = await BuildFilePathAsync(idDirectory, name, cancellationToken);

		await fileStorageRepository.SaveFileAsync(path, stream, cancellationToken);

		var manual = new ManualDto
		{
			Name = name,
			DateDownload = DateTimeOffset.UtcNow,
			IdDirectory = idDirectory,
			IdCategory = IdFileCategory,
			IdAuthor = idAuthor
		};

		return await manualRepository.InsertAsync(manual, cancellationToken);
	}

	public async Task<int> AddDirectoryAsync(string name, int? idParent, CancellationToken cancellationToken)
	{
		if (idParent.HasValue && !await manualDirectoryRepository.IsExistsAsync(idParent.Value, cancellationToken))
			throw new ArgumentInvalidException(nameof(idParent), "Родительской директории не существует");

		var directory = new ManualDirectoryDto
		{
			Name = name,
			IdParent = idParent,
		};

		if (await IsExistDirectoryAsync(directory, cancellationToken))
			throw new ArgumentInvalidException(name, "Директория с таким названием уже существует");
		
		return await manualDirectoryRepository.InsertAsync(directory, cancellationToken);
	}

	public async Task UpdateDirectoryAsync(int id, string name, CancellationToken cancellationToken)
	{
		var directory = await manualDirectoryRepository.GetOrDefaultAsync(id, cancellationToken)
			?? throw new ArgumentInvalidException(nameof(id), $"Директории с Id: {id} не существует");

		directory.Name = name;

		if (await IsExistDirectoryAsync(directory, cancellationToken))
			throw new ArgumentInvalidException(name, "Директория с таким названием уже существует");

		await manualDirectoryRepository.UpdateAsync(directory, cancellationToken);
	}

	public async Task<int> DeleteDirectoryAsync(int id, CancellationToken cancellationToken)
	{
		var directory = await manualDirectoryRepository.GetOrDefaultAsync(id, cancellationToken);

		if (directory is null)
			return 0;

		var path = fileStorageRepository.MakeFilePath(directoryFiles, IdFileCategory.ToString(),
			await BuildDirectoryPathAsync(id, cancellationToken));

		try
		{
			fileStorageRepository.DeleteDirectory(path, true);
		}
		catch (InvalidOperationException ex)
		{
			throw new ArgumentInvalidException(nameof(id), ex.Message);
		}
		
		return await manualDirectoryRepository.DeleteAsync(directory.Id, cancellationToken);
	}

	public async Task<int> DeleteFileAsync(int id, CancellationToken cancellationToken)
	{
		var manual = await manualRepository.GetOrDefaultAsync(id, cancellationToken);

		if (manual is null)
			return 0;

		var filePath = await BuildFilePathAsync(manual.IdDirectory, manual.Name, cancellationToken);
		
		fileStorageRepository.DeleteFile(filePath);

		return await manualRepository.DeleteAsync(manual.Id, cancellationToken);
	}

	public async Task<(Stream stream, string fileName)?> GetFileAsync(int id, CancellationToken cancellationToken)
	{
		var manual = await manualRepository.GetOrDefaultAsync(id, cancellationToken);

		if (manual is null)
			return null;

		var path = await BuildFilePathAsync(manual.IdDirectory, manual.Name, cancellationToken);
		var fileStream = new FileStream(path, FileMode.Open);
		return (fileStream, manual.Name);
	}

	private async Task<bool> IsExistDirectoryAsync(ManualDirectoryDto directory, CancellationToken cancellationToken)
	{
		var existingDirectory = await manualDirectoryRepository.GetOrDefaultAsync(directory.Name, directory.IdParent,
			cancellationToken);

		return existingDirectory is not null && directory.Id != existingDirectory.Id;
	}

	private async Task<string> BuildDirectoryPathAsync(int idDirectory, CancellationToken cancellationToken)
	{
		var directiories = await manualDirectoryRepository.GetAllAsync(cancellationToken);

		var directory = directiories.FirstOrDefault(d => d.Id == idDirectory)
			?? throw new ArgumentInvalidException(nameof(idDirectory), $"Директории с Id: {idDirectory} не существует");

		var pathSegments = new List<int> { directory.Id };

		while (directory.IdParent.HasValue)
		{
			directory = directiories.FirstOrDefault(d => d.Id == directory.IdParent.Value);
			pathSegments.Insert(0, directory!.Id);
		}

		return string.Join("/", pathSegments);
	}

	private async Task<string> BuildFilePathAsync(int idDirectory, string name, CancellationToken cancellationToken)
	{
		var directoryPath = await BuildDirectoryPathAsync(idDirectory, cancellationToken);

		return fileStorageRepository.MakeFilePath(directoryFiles, IdFileCategory.ToString(), Path.Combine(directoryPath, name));
	}
}