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", "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));
   }
}