using e_suite.API.Common.repository;
using e_suite.Database.Audit;
using e_suite.Database.Core.Tables.Activity;
using e_suite.Database.Core.Tables.Domain;
using e_suite.Messaging.Common;
using e_suite.Workflow.Core;
using e_suite.Workflow.Core.Extensions;
using e_suite.Workflow.Core.Interfaces;
using eSuite.Core.Clock;
using eSuite.Core.Enums;
using eSuite.Core.Miscellaneous;
using Microsoft.Extensions.Logging;
using System.Collections;

namespace e_suite.Service.WorkflowProcessor;

public class WorkflowProcessor : IWorkflowProcessor
{
    private readonly ILogger _logger;
    private readonly IClock _clock;
    private readonly IWorkflowTemplateRepository _workflowTemplateRepository;
    private readonly IWorkflowConverter _workflowConverter;
    private readonly IActivityRepository _activityRepository;
    private readonly IActivityMessageSender _activityMessageSender;

    public WorkflowProcessor(ILogger logger, IClock clock, IWorkflowTemplateRepository workflowTemplateRepository, IActivityRepository activityRepository, IWorkflowConverter workflowConverter, IActivityMessageSender activityMessageSender)
    {
        _logger = logger;
        _clock = clock;
        _workflowTemplateRepository = workflowTemplateRepository;
        _activityRepository = activityRepository;
        _workflowConverter = workflowConverter;
        _activityMessageSender = activityMessageSender;
    }

    public async Task ProgressActivity(
        AuditUserDetails auditUserDetails,
        GeneralIdRef activityId,
        CancellationToken cancellationToken
    )
    {
        _logger.LogInformation("{DateTime}: Progressing Activity {messageId}", _clock.GetNow, activityId);

        var activityInstance = await _activityRepository.GetActivityInstanceAsync(activityId, cancellationToken);
        if (activityInstance == null)
        {
            _logger.LogInformation("{DateTime}: Unable to find activity {messageId}", _clock.GetNow, activityId);
            throw new InvalidDataException("activityInstance not found");
        }
        
        if (activityInstance.ActivityState == ActivityState.Cancelled)
        {
            _logger.LogInformation("{DateTime}: Activity {messageId} is cancelled, skipping", _clock.GetNow,
                activityId);
            return;
        }

        if (activityInstance.ActivityState == ActivityState.Completed)
        {
            _logger.LogInformation("{DateTime}: Activity {messageId} is already completed, skipping", _clock.GetNow,
                activityId);
            return;
        }

        WorkflowVersion workflowVersion = _workflowConverter.DeserialiseFromDatabase(activityInstance.WorkflowVersion);
        
        var hasCompletableTask = false;

        await _activityRepository.TransactionAsync(async () =>
        {
            switch (activityInstance.ActivityState)
            {
                case ActivityState.Pending:
                {
                    hasCompletableTask = await StartActivity(auditUserDetails, cancellationToken, activityInstance,
                        workflowVersion);
                    break;
                }
                case ActivityState.Active:
                    hasCompletableTask = await ProgressRunningActivity(auditUserDetails, cancellationToken, activityInstance,
                        workflowVersion);

                    if (activityInstance.Tasks.All(x => x.ActivityState == ActivityState.Completed))
                    {
                        activityInstance.ActivityState = ActivityState.Completed;
                        await _activityRepository.UpdateActivityInstanceAsync(auditUserDetails, activityInstance, cancellationToken);
                    }
                    break;
            }
        });

        if (hasCompletableTask)
        {
            _activityMessageSender.ProgressActivity(activityId);
        }
    }

    private async Task<bool> ProgressRunningActivity(AuditUserDetails auditUserDetails, CancellationToken cancellationToken, Activity activityInstance, WorkflowVersion workflowVersion)
    {
        bool hasCompletableTask = false;
        foreach (var task in activityInstance.Tasks)
        {
            var taskDefinition = workflowVersion.FindTask(task.TaskGuid)!;
            if (await taskDefinition.ProgressTask(task, activityInstance.Tasks, workflowVersion, _clock))
            {
                hasCompletableTask = true;
            }
        }

        await _activityRepository.UpdateActivityTasksAsync(auditUserDetails, activityInstance.Tasks, cancellationToken);

        return hasCompletableTask;
    }

    private async Task<bool> StartActivity(
        AuditUserDetails auditUserDetails,
        CancellationToken cancellationToken,
        Activity? activityInstance,
        WorkflowVersion workflowVersion
    )
    {
        bool hasCompletableTask = false;
        activityInstance.ActivityState = ActivityState.Active;
        await _activityRepository.UpdateActivityInstanceAsync(auditUserDetails, activityInstance,
            cancellationToken);

        var tasks = await PlanTaskExecution(auditUserDetails, activityInstance, workflowVersion, cancellationToken);

        await StartInitialTasks(auditUserDetails, tasks, workflowVersion, cancellationToken);

        if (tasks.Any(task => task.ActivityState == ActivityState.ReadyToComplete))
        {
            hasCompletableTask = true;
        }

        return hasCompletableTask;
    }

    private async Task StartInitialTasks(AuditUserDetails auditUserDetails, ICollection<ActivityTask> tasks, WorkflowVersion workflowVersion, CancellationToken cancellationToken)
    {
        var startTasks = workflowVersion.GetStartTasks().ToList();
        var startTaskGuids = startTasks.Select(t => t.Guid).ToHashSet();

        foreach (var task in tasks)
        {
            if (startTaskGuids.Contains(task.TaskGuid))
            {
                var taskDefinition = workflowVersion.FindTask(task.TaskGuid)!;
                await taskDefinition.StartTask(task, _clock);
            }
        }

        await _activityRepository.UpdateActivityTasksAsync(auditUserDetails, tasks, cancellationToken);
    }

    private async Task<ICollection<ActivityTask>> PlanTaskExecution(AuditUserDetails auditUserDetails, Activity activityInstance, WorkflowVersion workflowVersion, CancellationToken cancellationToken)
    {
        var activityOrdinalCounter = 1;

        //Create task instances for the activity
        var activityTasks = new List<ActivityTask>();
        
        foreach (var task in workflowVersion.Tasks)
        {
            var activityTask = new ActivityTask
            {
                Guid = Guid.NewGuid(),
                Activity = activityInstance,
                ActivityState = ActivityState.Pending,
                TaskGuid = task.Guid,
                TaskType = task.GetType().FullName!,
                TaskName = task.Name,
                ActivityOrdinal = activityOrdinalCounter++,
            };

            if (task is IAssignees assignableTask)
                activityTask.Assignments = await GenerateAssignmentForTask(activityTask, assignableTask, auditUserDetails, cancellationToken );

            activityTasks.Add(activityTask);
            
        }
        await _activityRepository.AddActivityTasksAsync(auditUserDetails, activityTasks, cancellationToken);

        return activityTasks;
    }

    private async Task<ICollection<ActivityAssignment>> GenerateAssignmentForTask(ActivityTask activityTask, IAssignees task, AuditUserDetails auditUserDetails, CancellationToken cancellationToken)
    {
        // Find the IAssignees<T> interface implemented by this instance
        var typedInterface = task.GetType()
            .GetInterfaces()
            .FirstOrDefault(i =>
                i.IsGenericType &&
                i.GetGenericTypeDefinition() == typeof(IAssignees<,>));

        if (typedInterface == null)
            throw new InvalidOperationException("Task does not implement IAssignees<T>.");

        // Get the Assignees property
        var assigneesProperty = typedInterface.GetProperty("Assignees");
        var assignees = (IEnumerable)assigneesProperty.GetValue(task);

        var results = new List<ActivityAssignment>();

        foreach (var assignee in assignees)
        {
            var typedAssignee = (ITaskAssignee)assignee;


            var activityAssignment = new ActivityAssignment
            {
                Guid = Guid.NewGuid(),
                Raci = typedAssignee.Raci,
                Role = typedAssignee.Role!,
                User = typedAssignee.User!,
            };

            if (typedAssignee is IBypassable bypassableAssignee)
            {
                activityAssignment.Bypassable = bypassableAssignee.Bypassable;
            }

            if (typedAssignee is IApprovalTaskAssignee approvalAssignee)
            {
                activityAssignment.AllowNoVerdict = approvalAssignee.AllowNoVerdict;
            }

            results.Add(activityAssignment);
        }
        
        return results;
    }
}