using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using BO.AppServer.Business.Exceptions;
using BO.AppServer.Business.Mapper;
using BO.AppServer.Business.Services.Base;
using BO.AppServer.Data.Entity;
using BO.AppServer.Data.Repository;
using BO.AppServer.Metadata.Configuration;
using BO.AppServer.Metadata.Dto;
using BO.AppServer.Metadata.Enums;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Quadarax.Foundation.Core.Data;
using Quadarax.Foundation.Core.Data.Interface;
using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;

namespace BO.AppServer.Business.Services
{
    public class DocumentService : UserContextService
    {
        #region *** Private fields ***
        private IFileSystem _fileSystem;

        private WorkspaceRepo _repoWorkspace;
        private MetadocumentRepo _repoDocument;
        private MimeTypeRepo _repoMType;
        private ArtifactRepo _repoArtifact;

        private StorageService _srvStorage;
        #endregion

        #region *** Constructors ***
        public DocumentService(
            WorkspaceRepo repoWorkspace,
            MetadocumentRepo repoDocument,
            MimeTypeRepo repoMType,
            ArtifactRepo repoArtifact,
            StorageService srvStorage,
            IFileSystem fileSystem,
            IOptions<Configuration> config,
            UserRepo repoUser, 
            UserManager<IdentityUser> userManager, 
            IPrincipal currentPrincipal, 
            ILoggerFactory logger) : base(config, repoUser, userManager, currentPrincipal, logger)
        {
            _repoWorkspace = repoWorkspace ?? throw new ArgumentNullException(nameof(repoWorkspace));
            _repoDocument = repoDocument ?? throw new ArgumentNullException(nameof(repoDocument));
            _repoMType = repoMType ?? throw new ArgumentNullException(nameof(repoMType));
            _repoArtifact = repoArtifact ??  throw new ArgumentNullException(nameof(repoArtifact));
            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));

            _srvStorage = srvStorage ?? throw new ArgumentNullException(nameof(srvStorage));
        }
        #endregion

        #region *** Public Operations ***

        public async Task<ResultValueDto<DocumentRDto>> GetDocumentAsync(long workspaceId, long documentId)
        {
            var document = CheckAndGetDocument(workspaceId, documentId);
            return new ResultValueDto<DocumentRDto>(document.Map<Metadocument, DocumentRDto>());

        }

        public async Task<ResultValueDto<DocumentRDto>> GetDocumentAsync(long workspaceId, IdentificationTypeEnum identificationType, string identificationValue)
        {
            var document = identificationType == IdentificationTypeEnum.Id ? 
                _repoDocument.Get(ArgumentAsLong(nameof(identificationValue), identificationValue)) 
                : _repoDocument.GetByName(workspaceId, identificationValue);

            if (document == null)
                throw new NoDataException();

            return new ResultValueDto<DocumentRDto>(document.Map<Metadocument, DocumentRDto>());

        }

        public async Task<ResultsValueDto<DocumentRDto>> GetAllDocumentsAsync(long workspaceId,  DocumentsScopeEnums scope, PagingDto paging)
        {
            CheckAndGetWorkspace(workspaceId);
            IList<Metadocument> documents = new List<Metadocument>();
            switch (scope)
            {
                case DocumentsScopeEnums.All:
                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Pending:
                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.Pending).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Done:
                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.DoneOk).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Working:
                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.Processing).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Failed:
                    documents = _repoDocument.Query(paging).Where(x => x.Structure.WorkspaceId == workspaceId && x.Status == (int)DocumentStatusEnum.DoneFail).OrderBy(x => x.Name).ToList();
                    break;
            }

            return new ResultsValueDto<DocumentRDto>(documents.MapList<Metadocument, DocumentRDto>());
        }


        public async Task<ResultValueDto<DocumentRDto>> GetDocumentByNameAsync(long workspaceId,  string name)
        {
            CheckAndGetWorkspace(workspaceId);
            IList<Metadocument> documents = new List<Metadocument>();
            var document = _repoDocument.GetByName(workspaceId, name);
            if (document == null)
                throw new NoDataException();
            return new ResultValueDto<DocumentRDto>(document.Map<Metadocument, DocumentRDto>());
        }

        public async Task<ResultsValueDto<DocumentRDto>> GetAllDocumentsAsync(DocumentsScopeEnums scope, PagingDto paging)
        {
            IList<Metadocument> documents = new List<Metadocument>();
            switch (scope)
            {
                case DocumentsScopeEnums.All:
                    documents = _repoDocument.Query(paging).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Pending:
                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.Pending).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Done:
                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.DoneOk).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Working:
                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.Processing).OrderBy(x => x.Name).ToList();
                    break;
                case DocumentsScopeEnums.Failed:
                    documents = _repoDocument.Query(paging).Where(x => x.Status == (int)DocumentStatusEnum.DoneFail).OrderBy(x => x.Name).ToList();
                    break;
            }

            return new ResultsValueDto<DocumentRDto>(documents.MapList<Metadocument, DocumentRDto>());
        }

        public async Task<ResultValueDto<DocumentRDto>> CreateDocumentAsync(long workspaceId,  DocumentCDto document)
        {
            var workspace = CheckAndGetWorkspace(workspaceId);
            var exists = _repoDocument.Query(new Paging())
                .Any(x => x.Structure.WorkspaceId == workspaceId && x.Name == document.Name);
            if (exists)
            {
                var exc = new EntityAlreadyExistsException(typeof(Metadocument), document.Name);
                Log.LogWarning(exc.Message);
                throw exc;
            }

            var newDocument = _repoDocument.New();
            document.CopyToDto(newDocument);
            newDocument.Created = DateTime.Now;
            newDocument.CreatedByUser = GetCurrentUser();
            newDocument.Status = (int)DocumentStatusEnum.New;
            newDocument.StructureId = workspace.Structures.First().Id;
            _repoDocument.Set(newDocument);
            _repoDocument.Commit();

            return new ResultValueDto<DocumentRDto>(newDocument.Map<Metadocument, DocumentRDto>());
        }

        public async Task<ResultsValueDto<ArtifactRDto>> ClearDocumentArtifactsAsync(long workspaceId, long documentId, bool force)
        {
        
            var document = CheckAndGetDocument(workspaceId, documentId);
            CheckDocumentStatusForUpdate(document, force);


            var artifacts = _repoArtifact.GetArtifacts(documentId).ToArray(); // must be to array
            foreach (var artifact in artifacts)
            {
                await _srvStorage.DeleteContent(artifact.Id);
                _repoArtifact.Remove(artifact.Id);
                Log.LogInformation($"Document '{document.Name}' ({documentId}) artifact '{artifact.Name + artifact.Extension}' ({artifact.Id}) removed.");
            }
            
            document.Modified = DateTime.Now;
            document.ModifiedByUser = GetCurrentUser();
            _repoDocument.Set(document);
            _repoArtifact.Commit();
            _repoDocument.Commit();

            return new ResultsValueDto<ArtifactRDto>(artifacts.MapList<Artifact, ArtifactRDto>());

        }



        public async Task<ResultValueDto<ArtifactRDto>> AppendArtifactAsync(long workspaceId, long documentId,
            ArtifactCDto artifact,bool replace, ArtifactTypeEnum type, Stream content)
        {
            //TODO: Rewrite better way - mainly rollback scenario

            var document = CheckAndGetDocument(workspaceId, documentId);
            var existingArtifact = _repoArtifact.GetArtifactByName(documentId, artifact.Name, artifact.Extension);
            if (existingArtifact!=null && !replace)
            {
                var exc = new EntityAlreadyExistsException(typeof(Artifact), artifact.Name + "." + artifact.Extension);
                Log.LogWarning(exc.Message);
                throw exc;
            }

            var mtype = _repoMType.GetByMimeType(artifact.MimeType);
            if (mtype == null)
            {
                var exc = new EntityNotExistsException(typeof(MimeType), artifact.MimeType, 0);
                Log.LogWarning(exc.Message);
                throw exc;
            }

            var newArtifact = (existingArtifact != null && replace) ? existingArtifact : _repoArtifact.New();
            
            newArtifact.Name = artifact.Name;
            newArtifact.Extension = artifact.Extension;
            newArtifact.Length = content.Length;
            newArtifact.Metadocument = document;
            newArtifact.MimeType = mtype;
            newArtifact.Created = DateTime.Now;
            newArtifact.CreatedByUser = GetCurrentUser();
            newArtifact.Type = (int)type;
            _repoArtifact.Set(newArtifact);
            document = _repoDocument.Get(documentId);
            document.Modified = DateTime.Now;
            document.ModifiedByUser = GetCurrentUser();
            _repoDocument.Set(document);
            _repoArtifact.Commit();
            _repoDocument.Commit();

            await _srvStorage.PushContentAsync(newArtifact.Id, content);

            return new ResultValueDto<ArtifactRDto>(newArtifact.Map<Artifact, ArtifactRDto>());

        }



        public async Task<Tuple<Stream, string>> GetArtifactAsync(long workspaceId, long documentId, long artifactId)
        {
            var artifact = CheckAndGetArtifact(workspaceId, documentId, artifactId);
            var result = await _srvStorage.PopContentAsync(artifact.Id);

            return new Tuple<Stream, string>(result.Item1, artifact.MimeType.Mimetype1);

        }

        public ArtifactCDto CreateArtifactDto(string fileName, string contentType)
        {
            var result = new ArtifactCDto();
            result.Extension = _fileSystem.Path.GetExtension(fileName);
            result.MimeType = contentType;
            result.Name = _fileSystem.Path.GetFileNameWithoutExtension(fileName);
            return result;
        }
        #endregion

        #region *** Private Operations ***

        private Workspace CheckAndGetWorkspace(long workspaceId)
        {
            var workspace = _repoWorkspace.Get(workspaceId);
            if (workspace == null)
            {

                var exc = new EntityNotExistsException(typeof(Workspace), nameof(Workspace.Id), workspaceId);
                Log.LogWarning(exc.Message);
                throw exc;
            }

            return workspace;
        }

        private Metadocument CheckAndGetDocument(long workspaceId, long documentId)
        {
            CheckAndGetWorkspace(workspaceId);
            var document = _repoDocument.Get(workspaceId ,documentId);
            if (document == null)
            {

                var exc = new EntityNotExistsException(typeof(Metadocument), nameof(Metadocument.Id), documentId);
                Log.LogWarning(exc.Message);
                throw exc;
            }

            return document;
        }

        private void CheckDocumentStatusForUpdate(Metadocument document, bool force)
        {
            if (document == null)
                throw new ArgumentNullException(nameof(document));

            var currentStatus = (DocumentStatusEnum)document.Status;
            var allowedStatus = new DocumentStatusEnum[]
            {
                DocumentStatusEnum.New,
                DocumentStatusEnum.Pending,
            };

            if (!allowedStatus.Contains(currentStatus))
            {
                var message = $"Document '{document.Name}' [{document.Id}] has not allowed status '{currentStatus}' for operation UPDATE. Allowed statuses are {string.Join(",", allowedStatus)}.";
                Log.LogWarning(message +  (force ? " Skip due Force flag." : string.Empty));
                if (!force)
                    throw new InvalidDocumentStateException(document.Name, document.Id, currentStatus, "update");
            }
            
        }
        

        private Artifact CheckAndGetArtifact(long workspaceId, long documentId, long artifactId)
        {
            CheckAndGetWorkspace(workspaceId);
            CheckAndGetDocument(workspaceId, documentId);
            var artifact = _repoArtifact.GetArtifact(documentId, artifactId);
            if (artifact == null)
            {
                var exc = new EntityNotExistsException(typeof(Artifact), nameof(Artifact.Id), artifactId);
                Log.LogWarning(exc.Message);
                throw exc;
            }
            return artifact;
        }
        #endregion
    }
}
