using IWS5.Core.DTO;
using IWS5.Core.Enums;
using IWS5.Importer.Abstractions.FileSystem;
using IWS5.Core.Models.Request;
using IWS5.Importer.Abstractions.Interfaces;
using IWS5.Importer.Abstractions.Constants;
using IWS5.Importer.Abstractions.DTO;
using IWS5.Importer.Abstractions.Enums;
using IWS5.Importer.Abstractions.Services;
using Serilog.Context;
using ILogger = Serilog.ILogger;
using IProcessService = IWS5.Importer.Abstractions.Services.IProcessService;
using IWorkItemService = IWS5.Importer.Abstractions.Services.IWorkItemService;
using IFolderFieldService = IWS5.Importer.Abstractions.Services.IFolderFieldService;

namespace IWS5.Importer.Worker.Processors;

public class WorkItemProcessor(
    ILogger logger,
    IXmlParser xmlParser,
    IWorkItemService workItemService,
    IFolderMover folderMover,
    IFileMover fileMover,
    IGeneralImportSettingsProvider settingsProvider,
    IDocumentValidator documentValidator,
    IXmlValidator xmlValidator,
    IDocumentFieldService documentFieldService,
    IFolderFieldService folderFieldService,
    IDocumentCabinetService documentCabinetService,
    IProcessService processService,
    IFileConverterService fileConverterService,
    IPdfPageGenerator<List<(string filePath, string reason)>> pdfGenerator) : IWorkItemProcessor
{
    private readonly IXmlParser _xmlParser = xmlParser;
    private readonly IWorkItemService _workItemService = workItemService;
    private readonly IFolderMover _folderMover = folderMover;
    private readonly IFileMover _fileMover = fileMover;
    private readonly IDocumentValidator _documentValidator = documentValidator;
    private readonly IXmlValidator _xmlValidator = xmlValidator;
    private readonly IDocumentFieldService _documentFieldService = documentFieldService;
    private readonly IFolderFieldService _folderFieldService = folderFieldService;
    private readonly IProcessService _processService = processService;
    private readonly IFileConverterService _fileConverterService = fileConverterService;
    private readonly IPdfPageGenerator<List<(string filePath, string reason)>> _pdfGenerator = pdfGenerator;
    private Dictionary<int, List<DocumentFieldDto>>? _documentFields;
    private Dictionary<int, List<FolderFieldDto>>? _folderFields;
    private readonly string _importDir = settingsProvider.Settings.ImportDirs;
    private readonly string _backupLocation = settingsProvider.Settings.CopyLocation;
    private readonly string xmlExt = "." + SupportedExtension.Xml.ToString().ToLowerInvariant();
    private readonly string pdfExt = "." + SupportedExtension.Pdf.ToString().ToLowerInvariant();
    private const string done = "done.txt";
    private const string marker = ".marker";
    private const string errorFileReportName = "ErrorReport.pdf";

    /// <summary>
    /// Processes the folder files in the root folder (level 1). Files with XML/PDF pairs are processed 
    /// using data from the XML file while subfolders (specific work items) are processed separately.
    /// </summary>
    public async void ProcessingFolderFiles(string processFolderPath, List<ProcessDto>? processInfo, CancellationToken stoppingToken)
    {
        // Key arranged by cabinetId
        _documentFields = await _documentFieldService.GetValidDocumentFieldsAsync();
        // Key arranged by processId
        _folderFields = await _folderFieldService.GetValidFolderFieldsAsync();


        string processName = Path.GetFileName(processFolderPath);
        string processXmlPath = Path.Combine(processFolderPath, $"{processName}{xmlExt}");
        string forbiddenProcessName = Path.Combine(processFolderPath, $"{processName}{pdfExt}");

        // Parse XML data from the process folder
        var processXmlData = new Dictionary<string, Dictionary<string, string>>();
        if (File.Exists(processXmlPath))
        {
            logger.Information("Found process XML file: {ProcessXmlPath}", processXmlPath);
            processXmlData = _xmlParser.ParseAndUpdateXml(processXmlPath, processXmlData);
        }

        if (File.Exists(forbiddenProcessName))
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessingFolderFiles)))
                logger.Error("Found forbidden file with the same name as a process is: {ForbidenProcessName}", forbiddenProcessName);
            _fileMover.MoveFilesToError([forbiddenProcessName]);
        }
        var process = await _processService.GetProcessByNameAndVersionAsync(processName);

        var groupedFiles = await GroupFilesAsync(processFolderPath, process?.ProcessID ?? 0, stoppingToken, processXmlData);

        if (groupedFiles.Any())
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessingFolderFiles)))
                logger.Information("Found {Count} groups of files in folder {ProcessFolderPath}", groupedFiles.Count, processFolderPath);
            CreateWorkItemAsync(groupedFiles, processXmlData, processFolderPath, stoppingToken);
        }
        else
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessingFolderFiles)))
                logger.Information("No groups of files found in folder {ProcessFolderPath}", processFolderPath);
        }

        // Processing subfolders for the specific process
        if (processInfo is not null)
        {
            foreach (var workItemFolder in Directory.GetDirectories(processFolderPath))
            {
                stoppingToken.ThrowIfCancellationRequested();

                logger.Information("Processing work item folder: {WorkItemFolder}", workItemFolder);
                await ProcessWorkItemSubfolder(workItemFolder, processXmlData, stoppingToken);
            }
        }
    }

    /// <summary>
    /// Processes a work item subfolder (level 3). It first checks for the marker file "Done.txt", then 
    /// parses any XML in the folder to update the data, and finally groups files for further processing.
    /// </summary>
    public async Task ProcessWorkItemSubfolder(
        string workItemFolder, Dictionary<string, Dictionary<string, string>> processXmlData, CancellationToken stoppingToken)
    {
        if (!File.Exists(Path.Combine(workItemFolder, done)))
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessWorkItemSubfolder)))
                logger.Warning("Missing Done.txt in work item folder: {WorkItemFolder}", workItemFolder);
            return;
        }

        string workItemXmlPath = Path.Combine(workItemFolder, $"{Path.GetFileName(workItemFolder)}{xmlExt}");
        var workItemXmlData = processXmlData;

        if (File.Exists(workItemXmlPath))
        {
            logger.Information("Found workitem XML file: {ProcessXmlPath}", workItemXmlPath);
            workItemXmlData = _xmlParser.ParseAndUpdateXml(workItemXmlPath, processXmlData);
        }

        try
        {
            await CreateWorkItemSubfoldersAsync(stoppingToken, workItemXmlData, workItemFolder);
        }
        catch (InvalidDataException ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessWorkItemSubfolder)))
                logger.Warning(ex, "Failed to process work item folder: {WorkItemFolder}", workItemFolder);
            if (Directory.Exists(workItemFolder))
            {
                Directory.Delete(workItemFolder, recursive: true);
            }
            return;
        }
        catch (Exception ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(ProcessWorkItemSubfolder)))
                logger.Error(ex, "Failed to process work item folder: {WorkItemFolder}", workItemFolder);

            var allFilesDel = Directory.GetFiles(workItemFolder, "*", SearchOption.AllDirectories).ToList();
            DeleteConvertedArtifacts(allFilesDel);

            _folderMover.MoveInvalidFolderToError(workItemFolder, null);
            return;
        }

        var allFiles = Directory.GetFiles(workItemFolder, "*", SearchOption.AllDirectories).ToList();
        DeleteConvertedArtifacts(allFiles);

        _folderMover.MoveProcessedFolder(workItemFolder, _backupLocation);
    }

    /// <summary>
    /// Groups files in a folder by their base name while excluding marker files.
    /// </summary>
    private async Task<Dictionary<string, List<string>>> GroupFilesAsync(string workItemFolder, int processID,
        CancellationToken cancellationToken, Dictionary<string, Dictionary<string, string>> xmlData,
        bool requireXmlAndPdf = true, List<string>? filesToExclude = null)
    {
        string processOrWorkItemName = Path.GetFileName(workItemFolder);

        // Step 1 — Filter relevant files
        var files = FilterInitialFiles(workItemFolder, processOrWorkItemName, filesToExclude);

        // Step 2 — Try converting eligible files
        await ConvertEligibleFilesAsync(files, processID, processOrWorkItemName, cancellationToken, xmlData);

        // Step 3 — Group files (with/without xml/pdf)
        var groupedFiles = GetGroupedFiles(workItemFolder, processOrWorkItemName, filesToExclude, requireXmlAndPdf);

        // Step 4 — Remove Done.txt if needed
        if (!requireXmlAndPdf)
            CleanupDoneFile(workItemFolder);

        return groupedFiles;
    }

    /// <summary>
    /// Creates work items for levels 1/2 using XML data. It validates the files, converts PDF content, 
    /// builds the work item request using XML data (instead of hardcoded mock data), and creates the work item.
    /// </summary>
    private async void CreateWorkItemAsync(
        Dictionary<string, List<string>> groupFiles,
        Dictionary<string, Dictionary<string, string>> workItemXmlData,
        string workItemFolder, CancellationToken stoppingToken)
    {
        foreach (var group in groupFiles)
        {
            stoppingToken.ThrowIfCancellationRequested();
            string groupName = group.Key;
            List<string> files = group.Value;

            using (LogContext.PushProperty("LogContext", nameof(CreateWorkItemAsync)))
                logger.Information("Creating work item for group {GroupName} in folder {WorkItemFolder}", groupName, workItemFolder);

            var (pdfFilePath, xmlFilePath) = FindFiles(files);

            if (!ValidateFiles(pdfFilePath, xmlFilePath, groupName, workItemFolder, files)) continue;
            logger.Information("Validation of the group {GroupName} in folder {WorkItemFolder} was successfull", groupName, workItemFolder);

            // Step 2: Read PDF content and convert to Base64
            string documentFileContent = ConvertFileToBase64(pdfFilePath);

            // Step 3: Build WorkItemRequest using data from XML
            var workItemRequest = await BuildWorkItemRequest(groupName, documentFileContent, pdfFilePath, xmlFilePath, workItemXmlData);

            if (workItemRequest.ProcessId == 0)
            {
                logger.Error("ProcessId is zero for group {GroupName} in folder {WorkItemFolder}", groupName, workItemFolder);
                DeleteConvertedArtifacts(files);
                MoveFilesToError(files);
                continue;
            }

            // Step 4: Create Work Item & Handle Errors
            if (!await TryCreateWorkItem(workItemRequest, groupName, workItemFolder, pdfFilePath, xmlFilePath))
            {
                logger.Warning("Moving files {count} to error folder", files.Count);
                DeleteConvertedArtifacts(files);
                MoveFilesToError(files);
                continue;
            }

            // Step 5: Move processed files
            logger.Information("Moving files {count} to processed folder", files.Count);
            DeleteConvertedArtifacts(files);
            MoveFilesToProcessed(files);
        }
    }

    /// <summary>
    /// Creates a work item for a subfolder (level 3) using XML data to map fields.
    /// </summary>
    private async Task CreateWorkItemSubfoldersAsync(
        CancellationToken stoppingToken,
        Dictionary<string, Dictionary<string, string>> workItemXmlData,
        string workItemFolder)
    {
        var selectedProcess = await GetProcess(workItemXmlData, workItemFolder);
        if (string.IsNullOrEmpty(selectedProcess.ProcessName))
            throw new InvalidOperationException($"No valid process found for group {workItemFolder}");

        var workItemRequest = new CreateWorkItemRequest
        {
            WorkItemName = Path.GetFileName(workItemFolder),
            ProcessId = selectedProcess.ProcessID,
            FolderFields = GetMappedFolderFields(workItemXmlData, selectedProcess.ProcessID).ToArray()
        };

        // Step 1: Process Documents using XML mapping
        workItemRequest.Documents = (await ProcessDocumentsAsync(stoppingToken, workItemXmlData, workItemFolder, selectedProcess)).ToArray();

        workItemRequest.FolderFields = workItemRequest.FolderFields.Any() ? workItemRequest.FolderFields : GetMappedFolderFields(workItemXmlData, selectedProcess.ProcessID).ToArray();

        // Step 2: Create Work Item
        await TryCreateWorkItem(workItemRequest, workItemRequest.WorkItemName, workItemFolder);
    }

    /// <summary>
    /// Converts a file to a Base64 string.
    /// </summary>
    private string ConvertFileToBase64(string? filePath)
    {
        if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
        {
            logger.Error($"Cannot convert file to Base64. File {filePath} is null or doesn't exist.");
            return string.Empty;
        }
        try
        {
            byte[] fileBytes = File.ReadAllBytes(filePath);
            return Convert.ToBase64String(fileBytes);
        }
        catch (Exception ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(ConvertFileToBase64)))
                logger.Error(ex, "Failed to read file: {FilePath}", filePath);
            return string.Empty;
        }
    }

    /// <summary>
    /// Returns a tuple of PDF and XML file paths for a group of files.
    /// </summary>
    private (string? pdfFile, string? xmlFile) FindFiles(List<string> files)
    {
        if (files.Count == 0) return (null, null);
        string? pdfFilePath = files.FirstOrDefault(f => f.EndsWith(pdfExt, StringComparison.OrdinalIgnoreCase));
        string? xmlFilePath = files.FirstOrDefault(f => f.EndsWith(xmlExt, StringComparison.OrdinalIgnoreCase));
        return (pdfFilePath, xmlFilePath);
    }

    /// <summary>
    /// Validates the PDF and XML files using provided validators.
    /// </summary>
    private bool ValidateFiles(
                                string? pdfFilePath,
                                string? xmlFilePath,
                                string groupName,
                                string workItemFolder,
                                List<string> files)
    {
        using (LogContext.PushProperty("LogContext", nameof(ValidateFiles)))
        {
            if (pdfFilePath == null)
            {
                logger.Warning("No PDF file found for group {GroupName} in folder {WorkItemFolder}", groupName, workItemFolder);
                MoveFilesToError(files);
                return false;
            }

            if (!_documentValidator.ValidatePdf(pdfFilePath))
            {
                logger.Warning("PDF file {PdfFilePath} is invalid.", pdfFilePath);
                MoveFilesToError(files);
                return false;
            }

            if (!_xmlValidator.ValidateXml(xmlFilePath, XmlValidationLevel.root.ToString()))
            {
                logger.Warning("XML file {XmlFilePath} is invalid or missing required elements.", xmlFilePath);
                MoveFilesToError(files);
            }
        }
        return true;
    }

    private (string? filePath, string? errorDetail) ValidateFilesForWorkitem(
                                string? pdfFilePath,
                                string? xmlFilePath)
    {
        (string? filePath, string? errorDetail) result = (null, null);
        using (LogContext.PushProperty("LogContext", nameof(ValidateFilesForWorkitem)))
        {
            logger.Information("Validate files - {PDFFile} and {XMLFile}", pdfFilePath, xmlFilePath);

            if (pdfFilePath == null)
            {
                result.filePath = pdfFilePath;
                result.errorDetail = $"No PDF file found";
                return result;
            }

            if (!_documentValidator.ValidatePdf(pdfFilePath))
            {
                result.filePath = pdfFilePath;
                result.errorDetail = $"Invalid PDF";
                return result;
            }

            if (!_xmlValidator.ValidateXml(xmlFilePath, XmlValidationLevel.workitem.ToString()))
            {
                result.filePath = xmlFilePath;
                result.errorDetail = $"Invalid XML";
                return result;
            }
        }
        return result;
    }

    /// <summary>
    /// Moves files to an error folder.
    /// </summary>
    private void MoveFilesToError(List<string> files)
    {
        try
        {
            using (LogContext.PushProperty("LogContext", nameof(MoveFilesToError)))
                logger.Warning("Moving files {FilePath} to error folder", files.Count);

            _fileMover.MoveFilesToError(files);
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Failed to move files {FilePath} to error folder", files.Count);
        }
    }

    /// <summary>
    /// Moves files to the processed folder.
    /// </summary>
    private void MoveFilesToProcessed(List<string> files)
    {
        foreach (var file in files)
        {
            try
            {
                using (LogContext.PushProperty("LogContext", nameof(MoveFilesToProcessed)))
                    logger.Information("Moving file {FilePath} to processed folder", file);
                _fileMover.MoveFileToProcessed(file, _backupLocation);
            }
            catch (Exception ex)
            {
                using (LogContext.PushProperty("LogContext", nameof(MoveFilesToProcessed)))
                    logger.Error(ex, "Failed to move file {FilePath} to processed folder", file);
            }
        }
    }

    /// <summary>
    /// Builds a WorkItemRequest by mapping XML data instead of using hardcoded (mock) values.
    /// </summary>
    private async Task<CreateWorkItemRequest> BuildWorkItemRequest(
        string groupName,
        string documentFileContent,
        string? pdfFilePath,
        string? xmlFilePath,
        Dictionary<string, Dictionary<string, string>> workItemXmlData)
    {
        if (pdfFilePath == null || xmlFilePath == null)
            throw new ArgumentNullException("PDF or XML path is null. Cannot build work item request.");

        // Update data from the XML file
        workItemXmlData = _xmlParser.ParseAndUpdateXml(xmlFilePath, workItemXmlData, true);

        var selectedProcess = await GetProcess(workItemXmlData, pdfFilePath);
        if (selectedProcess.ProcessID == 0)
        {
            using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                logger.Warning("No valid process found for group {GroupName}", groupName);
            return new();
        }

        ProcessDto processData = new ProcessDto
        {
            ProcessName = selectedProcess.ProcessName,
            ProcessVersion = selectedProcess.ProcessVersion,
            ProcessID = selectedProcess.ProcessID,
            DefaultDocTemplate = selectedProcess.DefaultDocTemplate
        };

        var (documentTemplateName, cabinetId, documentFields) = await BuildDocumentDataAsync(workItemXmlData, selectedProcess?.DefaultDocTemplate);

        string? workItemName = null;

        if (workItemXmlData.TryGetValue(XmlSectionKeys.Process, out var processSection) &&
            processSection.TryGetValue(XmlFieldKeys.WorkItemName, out var name))
        {
            workItemName = name;
        }

        workItemName = string.IsNullOrWhiteSpace(workItemName) ? groupName : workItemName;

        return new CreateWorkItemRequest
        {
            WorkItemName = workItemName,
            ProcessId = processData.ProcessID,
            Documents =
            [
                new CreateDocumentRequest
                {
                    DocumentName = Path.GetFileName(pdfFilePath),
                    DocumentTemplateId = cabinetId,
                    DocumentTemplateName = documentTemplateName ?? string.Empty,
                    DocumentFile = documentFileContent,
                    DocumentFields = documentFields.ToArray(),
                    DisplayOrder = 1
                }
            ],
            FolderFields = GetMappedFolderFields(workItemXmlData, selectedProcess.ProcessID).ToArray()
        };
    }

    /// <summary>
    /// Maps folder fields from the FOLDER section of the XML. If no XML data is available, returns default folder fields.
    /// </summary>
    private List<CreateFolderFieldValueRequest> GetMappedFolderFields(Dictionary<string, Dictionary<string, string>> xmlData, int processId)
    {

        var folderFields = new List<CreateFolderFieldValueRequest>();
        if (xmlData.TryGetValue(XmlSectionKeys.Folder, out var folderData))
        {
            foreach (var kvp in folderData)
            {
                if (_folderFields is not null && _folderFields.TryGetValue(processId, out var validFields))
                {
                    var matchingField = validFields.FirstOrDefault(f => f.FieldName == kvp.Key);

                    if (matchingField != null)
                    {
                        folderFields.Add(new CreateFolderFieldValueRequest
                        {
                            FolderFieldId = matchingField.Id,
                            FolderFieldName = matchingField.FieldName,
                            FolderFieldType = MapToFieldInstanceType(matchingField.DataType),
                            FolderFieldValue = kvp.Value
                        });
                    }
                    else
                    {
                        using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                            logger.Warning("Field {FieldName} with value {FieldValue} is not valid for process {ProcessId}", kvp.Key, kvp.Value, processId);
                    }
                }
            }
        }
        return folderFields;
    }

    /// <summary>
    /// Processes documents by mapping data from the DOCUMENT section of the XML.
    /// For each group, the document is built using XML data.
    /// </summary>
    private async Task<List<CreateDocumentRequest>> ProcessDocumentsAsync(
        CancellationToken stoppingToken,
        Dictionary<string, Dictionary<string, string>> workItemXmlData,
        string workItemFolder, ProcessDto? selectedProcess)
    {
        var documents = new List<CreateDocumentRequest>();
        var fileErrors = new List<(string filePath, string reason)>();

        var issue = _fileMover.GetUnsupportedFilesFor(workItemFolder);
        fileErrors.AddRange(issue);

        var excludedPaths = fileErrors.Select(e => e.filePath).ToList();
        var groupedFiles = await GroupFilesAsync(workItemFolder, selectedProcess?.ProcessID ?? 0, stoppingToken, workItemXmlData, requireXmlAndPdf: false, excludedPaths);

        foreach (var group in groupedFiles)
        {
            string groupName = group.Key;
            List<string> files = group.Value;

            var (pdfFilePath, xmlFilePath) = FindFiles(files);

            if (pdfFilePath is null)
                continue;

            var result = ValidateFilesForWorkitem(pdfFilePath, xmlFilePath);

            if (result.filePath is not null && result.errorDetail is not null)
            {
                fileErrors.Add((result.filePath, result.errorDetail));
                using (LogContext.PushProperty("LogContext", nameof(ProcessDocumentsAsync)))
                    logger.Information("Invalid file {file} with {error} found for group {GroupName} in folder {WorkItemFolder}", result.filePath, result.errorDetail, groupName, workItemFolder);
                continue;
            }

            if (xmlFilePath is not null)
            {
                workItemXmlData = _xmlParser.ParseAndUpdateXml(xmlFilePath, workItemXmlData, true);
            }

            string documentFileContent = ConvertFileToBase64(pdfFilePath);

            var (documentTemplateName, cabinetId, documentFields) = await BuildDocumentDataAsync(workItemXmlData, selectedProcess!.DefaultDocTemplate);

            documents.Add(new CreateDocumentRequest
            {
                DocumentName = Path.GetFileName(pdfFilePath) ?? groupName,
                DocumentTemplateId = cabinetId,
                DocumentTemplateName = documentTemplateName,
                DocumentFile = documentFileContent,
                DocumentFields = documentFields.ToArray(),
                DisplayOrder = documents.Count + 1
            });
        }

        if (!documents.Any() && groupedFiles.Any())
            throw new InvalidDataException($"No valid documents found for group {workItemFolder}");

        if (fileErrors.Any())
        {
            documents.Add(new CreateDocumentRequest
            {
                DocumentName = errorFileReportName,
                DocumentTemplateId = 1,
                DocumentTemplateName = "DefaultTemplate",
                DocumentFile = _pdfGenerator.Generate(fileErrors),
                DocumentFields = null,
                DisplayOrder = 0
            });
            MoveFilesToError(fileErrors.Select(f => f.filePath).ToList());
        }

        return documents;
    }

    /// <summary>
    /// Retrieves the process ID from the current process definition or defaults to 105.
    /// </summary>
    private async Task<ProcessDto> GetProcess(Dictionary<string, Dictionary<string, string>> workItemXmlData, string? pdfFilePath)
    {
        string? processName = null;
        int processVersion = 0;

        if (workItemXmlData.TryGetValue(XmlSectionKeys.Process, out var processSection))
        {
            if (processSection.TryGetValue(XmlFieldKeys.ProcessName, out var name))
            {
                processName = name;
            }

            if (processSection.TryGetValue(XmlFieldKeys.ProcessVersion, out var versionString) &&
                int.TryParse(versionString, out var parsedVersion))
            {
                processVersion = parsedVersion;
            }
        }

        // If process name is not found in XML, try to derive it from the file path
        if (string.IsNullOrWhiteSpace(processName))
        {
            var relativePath = Path.GetRelativePath(_importDir, pdfFilePath);
            var pathParts = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

            if (pathParts.Length >= 2)
            {
                processName = pathParts[0];
            }
        }

        // Find the process name
        var validProcesses = await _processService.GetValidProcessesAsync();
        var validProcess = validProcesses
                .Where(p => string.Equals(p.Key, processName, StringComparison.OrdinalIgnoreCase))
                .SelectMany(p => p.Value);

        if (!validProcess.Any())
        {
            using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                logger.Warning("No valid process found for {ProcessName}", processName);
            return new();
        }

        ProcessDto? selectedProcess;

        if (validProcess.Count() == 1)
        {
            selectedProcess = validProcess.FirstOrDefault();
        }
        else
        {
            if (processVersion == 0)
            {
                selectedProcess = validProcess
                    .OrderByDescending(dto => dto.ProcessVersion)
                    .FirstOrDefault();
            }
            else
            {
                selectedProcess = validProcess
                    .FirstOrDefault(dto => dto.ProcessVersion == processVersion);
            }
        }

        if (selectedProcess is null)
        {
            using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                logger.Warning("No valid process found for {ProcessName} with version {ProcessVersion}", processName, processVersion);
            return new();
        }
        return selectedProcess;
    }

    /// <summary>
    /// Attempts to create a work item and handles any errors by moving associated files to the error folder.
    /// </summary>
    private async Task<bool> TryCreateWorkItem(
        CreateWorkItemRequest workItemRequest,
        string groupName,
        string workItemFolder,
        string? pdfFilePath = null,
        string? xmlFilePath = null)
    {
        if (pdfFilePath == null || xmlFilePath == null)
            throw new ArgumentNullException("PDF or XML path is null. Cannot build work item request.");

        try
        {
            await _workItemService.CreateWorkItem(workItemRequest, "pdadmin");
            return true;
        }
        catch (Exception ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(TryCreateWorkItem)))
                logger.Error(ex, "Failed to create work item for group {GroupName} in folder {WorkItemFolder}", groupName, workItemFolder);
            _fileMover.MoveFilesToError([pdfFilePath, xmlFilePath]);
            return false;
        }
    }

    /// <summary>
    /// Overload for creating a work item when only a work item folder is provided (used in level 3 subfolder processing).
    /// </summary>
    private async Task<bool> TryCreateWorkItem(CreateWorkItemRequest workItemRequest, string groupName, string workItemFolder)
    {
        try
        {
            await _workItemService.CreateWorkItem(workItemRequest, "pdadmin");
            return true;
        }
        catch (Exception ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(TryCreateWorkItem)))
                logger.Error(ex, "Failed to create work item for group {GroupName} in folder {WorkItemFolder}", groupName, workItemFolder);
            // If desired, move the first document file to error folder
            var firstDocName = workItemRequest.Documents.FirstOrDefault()?.DocumentName;
            if (firstDocName != null)
            {
                MoveFilesToError(new List<string> { firstDocName });
            }
            return false;
        }
    }

    private FieldInstanceType MapToFieldInstanceType(int dataType)
    {
        return dataType switch
        {
            5 => FieldInstanceType.String,
            1 => FieldInstanceType.Number,
            6 => FieldInstanceType.Date,
            _ => FieldInstanceType.String
        };
    }

    private static bool TryMapFieldType(string? fieldTypeName, out ATDocumentFieldType fieldType)
    {
        fieldType = ATDocumentFieldType.String; // default fallback

        if (string.IsNullOrWhiteSpace(fieldTypeName))
            return false;

        switch (fieldTypeName.Trim().ToLowerInvariant())
        {
            case "boolean":
                fieldType = ATDocumentFieldType.Boolean;
                return true;
            case "integer":
                fieldType = ATDocumentFieldType.Integer;
                return true;
            case "number":
            case "decimal":
                fieldType = ATDocumentFieldType.Number;
                return true;
            case "date":
                fieldType = ATDocumentFieldType.Date;
                return true;
            case "string":
                fieldType = ATDocumentFieldType.String;
                return true;
            default:
                return false;
        }
    }
    private bool TryConvertValue(string value, ATDocumentFieldType expectedType, out object convertedValue)
    {
        convertedValue = null;

        try
        {
            switch (expectedType)
            {
                case ATDocumentFieldType.Boolean:
                    if (bool.TryParse(value, out var boolVal))
                    {
                        convertedValue = boolVal;
                        return true;
                    }
                    break;

                case ATDocumentFieldType.Integer:
                    if (int.TryParse(value, out var intVal))
                    {
                        convertedValue = intVal;
                        return true;
                    }
                    break;

                case ATDocumentFieldType.Number:
                    if (decimal.TryParse(value, out var decVal))
                    {
                        convertedValue = decVal;
                        return true;
                    }
                    break;

                case ATDocumentFieldType.Date:
                    if (DateTime.TryParse(value, out var dateVal))
                    {
                        convertedValue = dateVal;
                        return true;
                    }
                    break;

                case ATDocumentFieldType.String:
                    convertedValue = value;
                    return true;
            }
        }
        catch (Exception ex)
        {
            using (LogContext.PushProperty("LogContext", nameof(TryConvertValue)))
                logger.Error(ex, "Failed to convert value '{Value}' to type '{ExpectedType}'", value, expectedType);
        }

        return false;
    }

    private async Task<(string? documentTemplateName, int cabinetId, List<UpdateDocumentFieldRequest> fields)>
    BuildDocumentDataAsync(Dictionary<string, Dictionary<string, string>> workItemXmlData, int? fallbackTemplateId = null)
    {
        string? documentTemplateName = null;
        int cabinetId = 0;
        var documentFields = new List<UpdateDocumentFieldRequest>();

        if (workItemXmlData.TryGetValue(XmlSectionKeys.Document, out var documentData))
        {
            documentTemplateName = documentData.TryGetValue(XmlFieldKeys.DocCabinet, out var dt) ? dt : null;

            if (string.IsNullOrWhiteSpace(documentTemplateName))
            {
                var docCab = await documentCabinetService.GetCabinetDataAsync();
                documentTemplateName = docCab.FirstOrDefault(c => c.Value == fallbackTemplateId).Key ?? "DefaultTemplate";
                cabinetId = fallbackTemplateId ?? 1;
            }
            else
            {
                cabinetId = await documentCabinetService.GetCabinetIdByNameAsync(documentTemplateName);
            }

            if (_documentFields != null && _documentFields.TryGetValue(cabinetId, out var validFields))
            {
                foreach (var kvp in documentData)
                {
                    var fieldMeta = validFields.FirstOrDefault(f => f.FieldName == kvp.Key);
                    if (fieldMeta != null)
                    {
                        if (TryMapFieldType(fieldMeta.FieldDataType, out ATDocumentFieldType mappedType))
                        {
                            if (TryConvertValue(kvp.Value, mappedType, out var convertedValue))
                            {
                                documentFields.Add(new UpdateDocumentFieldRequest
                                {
                                    FieldName = kvp.Key,
                                    FieldType = mappedType,
                                    FieldValue = kvp.Value
                                });
                            }
                            else
                            {
                                using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                                    logger.Warning("Field '{FieldName}' in cabinet {CabinetId} has value '{FieldValue}' which is not convertible to {ExpectedType}",
                                        kvp.Key, cabinetId, kvp.Value, mappedType);
                            }
                        }
                        else
                        {
                            using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                                logger.Warning("Unrecognized FieldType '{FieldDataType}' for field '{FieldName}' in cabinet {CabinetId}",
                                    fieldMeta.FieldDataType, kvp.Key, cabinetId);
                        }
                    }
                    else
                    {
                        using (LogContext.PushProperty("LogContext", nameof(WorkItemProcessor)))
                            logger.Warning("Field {FieldName} with value {FieldValue} is not valid for cabinet {CabinetId}",
                                kvp.Key, kvp.Value, cabinetId);
                    }
                }
            }
        }
        else
        {
            var docCab = await documentCabinetService.GetCabinetDataAsync();
            documentTemplateName = docCab.FirstOrDefault(c => c.Value == fallbackTemplateId).Key ?? "DefaultTemplate";
            cabinetId = fallbackTemplateId ?? 1;
        }

        return (documentTemplateName, cabinetId, documentFields);
    }

    /// <summary>
    /// Deletes all .marker files and their corresponding .pdf files from the provided file list.
    /// </summary>
    private void DeleteConvertedArtifacts(List<string> files)
    {
        foreach (var file in files)
        {
            try
            {
                // If this is a PDF, check for a related .marker
                if (file.EndsWith(pdfExt, StringComparison.OrdinalIgnoreCase))
                {
                    var markerPath = file + marker;
                    if (File.Exists(markerPath))
                    {
                        File.Delete(file);
                        File.Delete(markerPath);
                        using (LogContext.PushProperty("LogContext", nameof(DeleteConvertedArtifacts)))
                            logger.Information("Deleted generated PDF file {PdfFile} and marker {MarkerFile}", file, markerPath);
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Failed to delete converted artifact {File}", file);
            }
        }
    }

    private List<string> FilterInitialFiles(string workItemFolder, string processOrWorkItemName, List<string>? filesToExclude)
    {
        return Directory.GetFiles(workItemFolder)
            .Where(f =>
            {
                string name = Path.GetFileName(f);
                return !name.Equals(done, StringComparison.OrdinalIgnoreCase) &&
                       (filesToExclude == null || !filesToExclude.Contains(f, StringComparer.OrdinalIgnoreCase));
            })
            .ToList();
    }

    private Dictionary<string, List<string>> GetGroupedFiles(string workItemFolder, string processOrWorkItemName, List<string>? filesToExclude, bool requirePair)
    {
        var files = Directory.GetFiles(workItemFolder)
            .Where(f =>
            {
                string name = Path.GetFileName(f);
                return !name.Equals(done, StringComparison.OrdinalIgnoreCase) &&
                       !name.Equals($"{processOrWorkItemName}{xmlExt}", StringComparison.OrdinalIgnoreCase) &&
                       !name.EndsWith(marker, StringComparison.OrdinalIgnoreCase) &&
                       (filesToExclude == null || !filesToExclude.Contains(f, StringComparer.OrdinalIgnoreCase));
            })
            .ToList();

        var grouped = files.GroupBy(f => Path.GetFileNameWithoutExtension(f));

        if (requirePair)
        {
            grouped = grouped
                .Where(g => g.Any(f => f.EndsWith(xmlExt, StringComparison.OrdinalIgnoreCase)) &&
                            g.Any(f => f.EndsWith(pdfExt, StringComparison.OrdinalIgnoreCase)));
        }

        return grouped.ToDictionary(g => g.Key, g => g.ToList());
    }

    private async Task ConvertEligibleFilesAsync(List<string> files, int processID, string processOrWorkItemName, CancellationToken token,
        Dictionary<string, Dictionary<string, string>> xmlData)
    {
        var filePathXml = files
            .FirstOrDefault(f => Path.GetFileName(f)
            .Equals($"{processOrWorkItemName}{xmlExt}", StringComparison.OrdinalIgnoreCase));

        if (filePathXml != null)
        {
            var result = _xmlValidator.ValidateXml(filePathXml, XmlValidationLevel.root.ToString());
            if (result)
                xmlData = _xmlParser.ParseAndUpdateXml(filePathXml, xmlData);

            files.Remove(filePathXml);
        }
        foreach (var file in files)
        {
            token.ThrowIfCancellationRequested();

            string ext = Path.GetExtension(file).TrimStart('.').ToLowerInvariant();
            string name = Path.GetFileName(file);
            string fileNameWithoutExt = Path.GetFileNameWithoutExtension(file);

            if (ext is "pdf" or "xml" || name.Equals($"{processOrWorkItemName}{xmlExt}", StringComparison.OrdinalIgnoreCase))
                continue;
            else
            {
                filePathXml = files
                    .FirstOrDefault(f => Path.GetFileName(f)
                    .Equals($"{fileNameWithoutExt}{xmlExt}", StringComparison.OrdinalIgnoreCase));
                if (filePathXml != null)
                {
                    var filePath = files.Where(files => files.Equals($"{fileNameWithoutExt}{xmlExt}", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
                    var result = _xmlValidator.ValidateXml(filePathXml, XmlValidationLevel.root.ToString());
                    if (result)
                        xmlData = _xmlParser.ParseAndUpdateXml(filePathXml, xmlData);
                }

                var processName = xmlData.Where(kvp => kvp.Key.Equals(XmlSectionKeys.Process, StringComparison.OrdinalIgnoreCase))
                    .SelectMany(kvp => kvp.Value)
                    .FirstOrDefault(kvp => kvp.Key.Equals(XmlFieldKeys.ProcessName, StringComparison.OrdinalIgnoreCase))
                    .Value;
                var processVersion = xmlData.Where(kvp => kvp.Key.Equals(XmlSectionKeys.Process, StringComparison.OrdinalIgnoreCase))
                    .SelectMany(kvp => kvp.Value)
                    .FirstOrDefault(kvp => kvp.Key.Equals(XmlFieldKeys.ProcessVersion, StringComparison.OrdinalIgnoreCase))
                    .Value;
                ProcessDto? processDto = null;
                if (!string.IsNullOrWhiteSpace(processName))
                {
                    processDto = await _processService.GetProcessByNameAndVersionAsync(processOrWorkItemName, int.Parse(processVersion));
                    if (processDto == null && processVersion != null)
                    {
                        processDto = await _processService.GetProcessByNameAndVersionAsync(processName, int.Parse(processVersion));
                    }
                }

                if (processDto != null && processDto.ProcessID == processID)
                {
                    if (!await _fileConverterService.ShouldConvertAsync(ext, processID))
                        continue;
                }
                else
                {
                    using (LogContext.PushProperty("LogContext", nameof(ConvertEligibleFilesAsync)))
                        logger.Warning("Process ID {ProcessID} does not match XML data for file {FileName}. Skipping conversion.", processID, file);
                    if (!await _fileConverterService.ShouldConvertAsync(ext, processID))
                    {
                        continue;
                    }
                }
            }

            string outputPdf = Path.ChangeExtension(file, pdfExt);
            string markerPath = outputPdf + marker;

            if (!File.Exists(outputPdf))
            {
                bool success = await _fileConverterService.TryConvertToPdfAsync(file, outputPdf, processID, token);
                if (success)
                {
                    File.Create(markerPath).Dispose();
                    using (LogContext.PushProperty("LogContext", nameof(ConvertEligibleFilesAsync)))
                        logger.Information("Converted {Input} to {Pdf}", file, outputPdf);
                }
                else
                {
                    using (LogContext.PushProperty("LogContext", nameof(ConvertEligibleFilesAsync)))
                        logger.Error("Failed to convert {Input}", file);
                }
            }
        }
    }

    private void CleanupDoneFile(string workItemFolder)
    {
        var donePath = Path.Combine(workItemFolder, done);
        if (File.Exists(donePath))
        {
            try
            {
                File.Delete(donePath);
                using (LogContext.PushProperty("LogContext", nameof(CleanupDoneFile)))
                    logger.Information("Deleted Done.txt from {Folder}", workItemFolder);
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Failed to delete Done.txt in {Folder}", workItemFolder);
            }
        }
        else
        {
            using (LogContext.PushProperty("LogContext", nameof(CleanupDoneFile)))
                logger.Warning("Done.txt not found in {Folder}", workItemFolder);
        }
    }
}
