using FluentValidation;
using IWS5.Core.Constants;
using IWS5.Core.DTO;
using IWS5.Core.Enums;
using IWS5.Core.Exceptions;
using IWS5.Core.Models;
using IWS5.Core.Models.Request;
using IWS5.Core.Services;
using IWS5.DatabaseAccess.Interfaces;
using IWS5.DatabaseAccess.IwsDataModels;
using Serilog;

namespace IWS5.Services;

public class DocumentService(
    IDocumentCabinetRepository documentCabinetRepository,
    IDocumentRepository documentRepository, 
    IRightsRepository rightsRepository,
    IWorkItemReferenceRepository workItemReferenceRepository,
    IProcessRepository processRepository,
    IValidator<UpdateDocumentFieldListRequest> updateDocumentFieldsRequestValidator,
    ILogger logger) : IDocumentService
{
    public async Task<Response<DocumentDTO>> GetDocument(DocumentRequest documentRequest)
    {
        logger.Information("Starting document retrieval for DocumentId {DocumentId} requested by User {UserName}",
            documentRequest.DocumentId, documentRequest.UserName);

        try
        {
            var dataResponse =
                await documentRepository.FindAsync(document => document.DocumentId == documentRequest.DocumentId);
            logger.Information("Successfully retrieved document data for DocumentId {DocumentId}",
                documentRequest.DocumentId);

            var isLocked = await workItemReferenceRepository.IsLockedForUserByWorkItemId(dataResponse.WorkItemId, documentRequest.UserName);

            if (isLocked)
            {
                logger.Error("User {UserName} is not allowed to view document for WorkItemId {WorkItemId} because it is locked", documentRequest.UserName, dataResponse.WorkItemId);
                throw new InvalidOperationException($"User {documentRequest.UserName} is not allowed to view document for WorkItemId {dataResponse.WorkItemId} because it is locked");
            }

            var responseModel = new DocumentDTO
            (
                id: dataResponse.DocumentId,
                documentName: dataResponse.DocumentName,
                document: Convert.ToBase64String(dataResponse.Document),
                displayOrder: dataResponse.DisplayOrder
            );

            return new Response<DocumentDTO>(responseModel, true, null);
        }
        catch (Exception ex)
        {
            logger.Error(ex,
                "Error occurred while retrieving document for DocumentId {DocumentId} requested by User {UserName}",
                documentRequest.DocumentId);
            throw;
        }
    }

    public async Task<Response<List<DocumentListItemDTO>>> ListDocumentFields(DocumentsRequest documentRequest)
    {
        bool isLocked = true;
        try
        {
            isLocked = await workItemReferenceRepository.IsLockedForUserByWorkItemReferenceId(documentRequest.WorkItemReferenceId, documentRequest.UserName);
        }
        catch (NotFoundException ex)
        {
            logger.Error(ex.Message);
            throw;
        }

        if (isLocked)
        {
            logger.Error("User {UserName} is not allowed to view document fields values for WorkItemReferenceId {WorkItemReferenceId} because it is locked", documentRequest.UserName, documentRequest.WorkItemReferenceId);
            throw new InvalidOperationException($"User {documentRequest.UserName} is not allowed to view document fields values for WorkItemId {documentRequest.WorkItemReferenceId} because it is locked");
        }

        var rights = await rightsRepository
            .ListUserRelatedRights(documentRequest.UserName, ObjectType.DocumentField, ATRightAllowRight.EditDocField);
        
        var idsAllowedToEdit = rights.Select(right => right.ObjectGuid).ToList();

        var dataResponse = await documentRepository.ListDocumentsByWorkItemReference(documentRequest);
        
        var documents = DocumentListItemDtos(dataResponse, idsAllowedToEdit);
        
        var groupedByTemplateName = documents.GroupBy(x => x.DocumentTemplateName);
        foreach (var template in groupedByTemplateName)
        {
            var fieldNames = template.SelectMany(x => x.DocumentTemplateFields)
                .Select(field => field.FieldName)
                .ToList();
            
            fieldNames.Add(DocumentTemplateTableConstants.DocumentIdColumnName);
            fieldNames.Add(DocumentTemplateTableConstants.InstanceIdColumnName);
            
            var documentIds = template.Select(x => x.DocumentId).ToArray();
            var fieldsObjects = await documentRepository.GetTemplateDataAsync(template.Key, fieldNames, documentIds);
            
            foreach (var fieldsObject in fieldsObjects)
            {
                var documentId = (int)fieldsObject[DocumentTemplateTableConstants.DocumentIdColumnName];
                var document = documents.FirstOrDefault(x => x.DocumentId == documentId);
                
                foreach (var fieldDto in document.DocumentTemplateFields)
                {
                    fieldDto.FieldValue = fieldsObject[fieldDto.FieldName].ToString();
                }
            }
        }
        
        return new Response<List<DocumentListItemDTO>>(documents, true, null);
    }

    public async Task<CodeResponse<DocumentListItemDTO>> RenameDocument(string userName, RenameDocumentRequest renameDocumentRequest) {
        var document = await documentRepository.FindAsync(document => document.DocumentId == renameDocumentRequest.DocumentId);

        var extension = "." + document.DocumentName?.Split('.').Last();
        if (renameDocumentRequest.NewName.EndsWith(extension)) {
            logger.Error("The new document name should not include the extension {Extension} because it will be appended to the new name upon rename.", extension);
            return new CodeResponse<DocumentListItemDTO>(ResponseCode.BadRequest, $"The new document name should not include the extension {extension} because it will be appended to the new name upon rename.");
        }
        renameDocumentRequest.NewName += extension;

        var isLocked = await workItemReferenceRepository.IsLockedForUserByWorkItemId(document.WorkItemId, userName);
        if (isLocked) {
            logger.Error("User {UserName} is not allowed to edit document for WorkItemId {WorkItemId} because it is locked", userName, document.WorkItemId);
            return new CodeResponse<DocumentListItemDTO>(ResponseCode.Forbidden, $"User {userName} is not allowed to edit document for WorkItemId {document.WorkItemId} because it is locked");
        }

        var process = await processRepository.FindAsync(process => process.ProcessId == renameDocumentRequest.ProcessId);

        var updateDocumentRights = await rightsRepository.ListUserRelatedRights(userName, ObjectType.Process, ATRightAllowRight.EditDocument, process.ObjectId);
        if (updateDocumentRights.Count == 0) {
            logger.Error("User {UserName} is not allowed to edit documents", userName);
            return new CodeResponse<DocumentListItemDTO>(ResponseCode.Forbidden, $"User {userName} is not allowed to edit documents");
        }

        var data = await documentRepository.RenameDocumentAsync(userName, renameDocumentRequest);

        var result = MapAtDocumentToDocumentListItemDTO(data);

        return new CodeResponse<DocumentListItemDTO>(ResponseCode.OK, null, result);
    }

    public async Task<CodeResponse<DocumentListItemDTO>> UpdateDocumentContent(string userName, UpdateDocumentContentRequest updateDocumentContentRequest) 
    {
        var process = await processRepository.FindAsync(process => process.ProcessId == updateDocumentContentRequest.ProcessId);

        var updateDocumentRights = await rightsRepository.ListUserRelatedRights(userName, ObjectType.Process, ATRightAllowRight.EditDocument, process.ObjectId);
        if (updateDocumentRights.Count == 0) {
            logger.Error("User {UserName} is not allowed to edit documents", userName);
            return new CodeResponse<DocumentListItemDTO>(ResponseCode.Forbidden, $"User {userName} is not allowed to edit documents");
        }
        
        var document = await documentRepository.FindAsync(document => document.DocumentId == updateDocumentContentRequest.DocumentId);

        var isLocked = await workItemReferenceRepository.IsLockedForUserByWorkItemId(document.WorkItemId, userName);
        if (isLocked) {
            logger.Error("User {UserName} is not allowed to edit document for WorkItemId {WorkItemId} because it is locked", userName, document.WorkItemId);
            return new CodeResponse<DocumentListItemDTO>(ResponseCode.Forbidden, $"User {userName} is not allowed to edit document for WorkItemId {document.WorkItemId} because it is locked");
        }

        var data = await documentRepository.UpdateDocumentContentAsync(userName, updateDocumentContentRequest);

        var result = MapAtDocumentToDocumentListItemDTO(data);

        return new CodeResponse<DocumentListItemDTO>(ResponseCode.OK, null, result);
    }
    
    public async Task UpdateDocumentFields(string userName, UpdateDocumentFieldListRequest updateDocumentFieldListRequest)
    {
        var document = await documentRepository
            .FindAsync(document => document.DocumentId == updateDocumentFieldListRequest.DocumentId, document => document.Cabinet);

        var isLocked = await workItemReferenceRepository.IsLockedForUserByWorkItemId(document.WorkItemId, userName);

        if (isLocked)
        {
            logger.Error("User {UserName} is not allowed to view document for WorkItemId {WorkItemId} because it is locked", userName, document.WorkItemId);
            throw new InvalidOperationException($"User {userName} is not allowed to view document for WorkItemId {document.WorkItemId} because it is locked");
        }

        await updateDocumentFieldsRequestValidator.ValidateAndThrowAsync(updateDocumentFieldListRequest);
        
        var rights = await rightsRepository
            .ListUserRelatedRights(userName, ObjectType.DocumentField, ATRightAllowRight.EditDocField);
        
        var cabinet = await documentCabinetRepository
            .FindAsync(cabinet => cabinet.CabinetId == updateDocumentFieldListRequest.DocumentTemplateId,
                cabinet => cabinet.AtdocumentCabinetDefs);
        
        var allowedToEditDocumentFields = cabinet.AtdocumentCabinetDefs
            .Where(field => rights.Any(right => right.ObjectGuid == field.ObjectId))
            .Select(field => field.FieldName);
        
        var invalidFields = updateDocumentFieldListRequest.FieldsToUpdate
            .Where(field => !allowedToEditDocumentFields.Contains(field.FieldName))
            .Select(field => field.FieldName)
            .ToList();
        
        if (invalidFields.Count != 0)
        {
            var invalidFieldsMessage = string.Join(", ", invalidFields);
            throw new ForbiddenAccessException($"The following fields are not allowed to be edited: {invalidFieldsMessage}");
        }

        if (document.CabinetId == updateDocumentFieldListRequest.DocumentTemplateId)
        {
            await documentRepository.UpdateDocumentFields(userName, updateDocumentFieldListRequest);
        }
        else
        {
            await documentRepository.ChangeTemplateForDocument(userName, document.Cabinet.CabinetName, updateDocumentFieldListRequest);
        }

    }
    
    public async Task<Response<String>> DeleteDocument(string userName, int documentId, bool permanently)
    {
        List<Atright> rights = null;
        try
        {
            var document = await documentRepository.FindAsync(d => d.DocumentId == documentId);
            if (document == null)
            {
                logger.Warning("Document with ID {DocumentId} could not be found or deleted.", documentId);
                throw new NotFoundException($"Document with ID {documentId} does not exist.");
            }
            var workItem = await workItemReferenceRepository.FindAsync(wReference => wReference.WorkItemId == document.WorkItemId, wRef => wRef.WorkItem);
            var process = await processRepository.FindAsync(process => process.ProcessId == workItem.WorkItem.ProcessId);
            rights = await rightsRepository
            .ListUserRelatedRights(userName, ObjectType.Process, ATRightAllowRight.DeleteDocument, process.ObjectId);
        }
        catch (Exception ex)
        {
            logger.Error($"Error occurred while checking user rights for document deletion {documentId} with error: {ex.Message}");
            logger.Error($"Full stackTrace: {ex.StackTrace}");
            throw;
        }
        
        if (rights.Count == 0)
        {
            logger.Error($"User {userName} is not allowed to delete documents");
            throw new ForbiddenAccessException($"User {userName} is not allowed to delete documents");
        }

        logger.Information("User {UserName} requested to delete document ID {DocumentId} (Permanently: {Permanently}).",
            userName, documentId, permanently);

        var dataResponse =
                await documentRepository.FindAsync(document => document.DocumentId == documentId, doc => doc.WorkItem);

        if (!string.IsNullOrEmpty(dataResponse.WorkItem.LockedBy))
            if (dataResponse.WorkItem.LockedBy != userName)
            {
                logger.Error("User {UserName} is not allowed to delete document for WorkItemId {WorkItemId} because it is locked", userName, dataResponse.WorkItemId);
                throw new InvalidOperationException($"User {userName} is not allowed to delete document for WorkItemId {dataResponse.WorkItemId} because it is locked");
            }

        var result = await documentRepository.DeleteDocumentAsync(documentId, permanently);

        if (!result)
        {
            logger.Warning("Document with ID {DocumentId} could not be found or deleted.", documentId);
        }

        return new Response<string>($"Document with ID {documentId} {(permanently ? "permanently deleted" : "marked as deleted")}", result);
    }

    public async Task<Response<DocumentListItemDTO>> AddDocument(string userName, AddDocumentRequest documentRequest)
    {
        var process = await processRepository.FindAsync(process => process.ProcessId == documentRequest.ProcessId);

        var attachDocumentRights = await rightsRepository
            .ListUserRelatedRights(userName, ObjectType.Process, ATRightAllowRight.AttachDocument, process.ObjectId);
        
        if (attachDocumentRights.Count == 0)
        {
            logger.Error("User {UserName} is not allowed to add documents", userName);
            throw new ForbiddenAccessException($"User {userName} is not allowed to add documents");
        }

        var data =  await documentRepository.AddDocumentAsync(userName, documentRequest);
        
        var templateFieldObjectIds = data.Cabinet.AtdocumentCabinetDefs.Select(field => field.ObjectId).ToArray();
        var editDocumentFieldRights = await rightsRepository
            .ListUserRelatedRights(userName, ObjectType.DocumentField, ATRightAllowRight.EditDocField, templateFieldObjectIds);
        var allowedToEditDocumentFieldIds = data.Cabinet.AtdocumentCabinetDefs
            .Where(field => editDocumentFieldRights.Any(right => right.ObjectGuid == field.ObjectId))
            .Select(field => field.ObjectId).Distinct().ToList();

        var result = MapAtDocumentToDocumentListItemDTO(data, allowedToEditDocumentFieldIds);
        
        return new Response<DocumentListItemDTO>(result, true, null);

    }
    
    private static List<DocumentListItemDTO> DocumentListItemDtos(List<Atdocument> dataResponse, List<Guid> idsAllowedToEdit)
    {
        return dataResponse
            .Select(document => MapAtDocumentToDocumentListItemDTO(document, idsAllowedToEdit))
            .ToList();
    }

    private static DocumentListItemDTO MapAtDocumentToDocumentListItemDTO(Atdocument document, List<Guid> idsAllowedToEdit = null)
    {
        return new DocumentListItemDTO
        {
            PopulatingContent = document.Document != null,
            DocumentId = document.DocumentId,
            DocumentName = document.DocumentName,
            DisplayOrder = document.DisplayOrder,
            DocumentTemplateId = document.CabinetId,
            DocumentTemplateName = document.Cabinet?.CabinetName,
            DocumentSize = document.DocumentSize,
            UserName = document.UserName,
            LastModified = document.LastModified,
            DocumentTemplateFields = document.Cabinet.AtdocumentCabinetDefs
                .Select(field => new CabinetFieldDto
                {
                    FieldId = field.FieldId,
                    FieldName = field.FieldName,
                    FieldType = (ATDocumentFieldType)field.FieldDataTypeId.Value,
                    FieldTypeName = field.FieldDataType,
                    IsEditable = idsAllowedToEdit.Contains(field.ObjectId)
                }).ToList()
        };
    }
}