using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Domain.Expressions;
using Workflow.Infrastructure.Persistence;

using TaskStatus = Workflow.Domain.Enums.TaskStatus;

namespace Workflow.Application.Engine;

/// <summary>
/// Core workflow engine that handles token propagation through the workflow graph.
/// Processes nodes by type: Start, End, Approval, Cc, Condition, Parallel, SubProcess.
/// </summary>
public class ProcessEngine
{
    private readonly WorkflowDbContext _dbContext;
    private readonly IServiceProvider _serviceProvider;
    private readonly ConditionEvaluator _conditionEvaluator = new();

    public ProcessEngine(WorkflowDbContext dbContext, IServiceProvider serviceProvider)
    {
        _dbContext = dbContext;
        _serviceProvider = serviceProvider;
    }

    /// <summary>
    /// Starts a workflow instance by finding the Start node and propagating tokens.
    /// </summary>
    public async Task StartAsync(WorkflowInstance instance)
    {
        // Load nodes and edges for this definition
        var nodes = await _dbContext.WorkflowNodes
            .Where(n => n.DefinitionId == instance.DefinitionId)
            .ToListAsync();

        // Find the start node
        var startNode = nodes.FirstOrDefault(n => n.NodeType == NodeType.Start)
            ?? throw new BusinessException("No start node found in workflow definition");

        // Set instance status to Running
        instance.Status = InstanceStatus.Running;

        // Create a token at the start node
        var token = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            NodeId = startNode.Id,
            Status = TokenStatus.Active,
        };
        _dbContext.WorkflowTokens.Add(token);

        await _dbContext.SaveChangesAsync();

        // Immediately process the start node
        await ProcessNodeAsync(instance, token, startNode);
    }

    /// <summary>
    /// Processes a node based on its type, propagating tokens accordingly.
    /// </summary>
    public async Task ProcessNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        switch (node.NodeType)
        {
            case NodeType.Start:
                await ProcessStartNodeAsync(instance, token, node);
                break;
            case NodeType.End:
                await ProcessEndNodeAsync(instance, token, node);
                break;
            case NodeType.Approval:
                await ProcessApprovalNodeAsync(instance, token, node);
                break;
            case NodeType.Cc:
                await ProcessCcNodeAsync(instance, token, node);
                break;
            case NodeType.Condition:
                await ProcessConditionNodeAsync(instance, token, node);
                break;
            case NodeType.Parallel:
                await ProcessParallelNodeAsync(instance, token, node);
                break;
            case NodeType.SubProcess:
                await ProcessSubProcessNodeAsync(instance, token, node);
                break;
            default:
                throw new BusinessException($"Unknown node type: {node.NodeType}");
        }
    }

    /// <summary>
    /// Completes a workflow task, executing actions and propagating tokens.
    /// </summary>
    public async Task CompleteTaskAsync(WorkflowTask task, TaskResult result)
    {
        // Update task status
        task.Status = result == TaskResult.Approved ? TaskStatus.Approved : TaskStatus.Rejected;
        task.Result = result.ToString().ToLowerInvariant();
        task.CompletedAt = DateTime.UtcNow;

        // Find the node for this task
        var node = await FindNodeAsync(task.NodeId);
        if (node is null)
            throw new BusinessException("Task node not found");

        // Execute actions based on result
        var hook = result == TaskResult.Approved ? "onApproved" : "onRejected";
        await ExecuteActionsSafely(node, hook, task.InstanceId, task.Result);

        // Find the token for this task
        var token = await _dbContext.WorkflowTokens.FindAsync(task.TokenId);

        // Find outgoing edges filtered by result
        var edges = await GetOutgoingEdgesAsync(node.Id);
        var targetEdge = result == TaskResult.Approved
            ? edges.FirstOrDefault(e => e.EdgeType == EdgeType.Approved)
                ?? edges.FirstOrDefault(e => e.EdgeType == EdgeType.Normal)
            : edges.FirstOrDefault(e => e.EdgeType == EdgeType.Rejected);

        if (targetEdge is null)
        {
            if (result == TaskResult.Rejected)
                throw new BusinessException("No reject edge found for this node");
            throw new BusinessException("No outgoing edge found for this node");
        }

        var instance = await _dbContext.WorkflowInstances.FindAsync(task.InstanceId)
            ?? throw new BusinessException("Instance not found");

        // Consume the task's token
        if (token is not null)
        {
            token.Status = TokenStatus.Consumed;
            token.CompletedAt = DateTime.UtcNow;
        }

        // Create new token at target node
        var newToken = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            NodeId = targetEdge.TargetNodeId,
            Status = TokenStatus.Active,
        };
        _dbContext.WorkflowTokens.Add(newToken);

        await _dbContext.SaveChangesAsync();

        // Find target node and process it
        var targetNode = await FindNodeAsync(targetEdge.TargetNodeId);
        if (targetNode is not null)
        {
            await ProcessNodeAsync(instance, newToken, targetNode);
        }
    }

    /// <summary>
    /// Handles the completion of a sub-process instance by propagating the parent token.
    /// </summary>
    public async Task HandleSubProcessCompletionAsync(WorkflowInstance childInstance)
    {
        if (childInstance.ParentInstanceId is null)
            return;

        var parentInstance = await _dbContext.WorkflowInstances
            .FindAsync(childInstance.ParentInstanceId.Value);

        if (parentInstance is null)
            return;

        // Find the token that was waiting for this sub-process
        var waitingToken = childInstance.ParentTokenId.HasValue
            ? await _dbContext.WorkflowTokens.FindAsync(childInstance.ParentTokenId.Value)
            : null;

        if (waitingToken is null)
            return;

        // Find the sub-process node
        var subProcessNode = await FindNodeAsync(waitingToken.NodeId);
        if (subProcessNode is null)
            return;

        // Consume the waiting token
        waitingToken.Status = TokenStatus.Consumed;
        waitingToken.CompletedAt = DateTime.UtcNow;

        // Find outgoing edge and propagate
        var edges = await GetOutgoingEdgesAsync(subProcessNode.Id);
        var edge = edges.FirstOrDefault();

        if (edge is not null)
        {
            var newToken = new WorkflowToken
            {
                Id = Guid.NewGuid(),
                InstanceId = parentInstance.Id,
                NodeId = edge.TargetNodeId,
                Status = TokenStatus.Active,
            };
            _dbContext.WorkflowTokens.Add(newToken);

            await _dbContext.SaveChangesAsync();

            var targetNode = await FindNodeAsync(edge.TargetNodeId);
            if (targetNode is not null)
            {
                await ProcessNodeAsync(parentInstance, newToken, targetNode);
            }
        }
    }

    // ============================================================
    // Node Processing Methods
    // ============================================================

    private async Task ProcessStartNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var edges = await GetOutgoingEdgesAsync(node.Id);
        if (edges.Count == 0)
            throw new BusinessException("Start node must have at least one outgoing edge");

        // Consume current token
        token.Status = TokenStatus.Consumed;
        token.CompletedAt = DateTime.UtcNow;

        // Create new token at the target of the first edge
        var targetEdge = edges.First();
        var newToken = new WorkflowToken
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            NodeId = targetEdge.TargetNodeId,
            Status = TokenStatus.Active,
        };
        _dbContext.WorkflowTokens.Add(newToken);

        await _dbContext.SaveChangesAsync();

        // Process the target node
        var targetNode = await FindNodeAsync(targetEdge.TargetNodeId);
        if (targetNode is not null)
        {
            await ProcessNodeAsync(instance, newToken, targetNode);
        }
    }

    private async Task ProcessEndNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        // Consume the token
        token.Status = TokenStatus.Consumed;
        token.CompletedAt = DateTime.UtcNow;

        await _dbContext.SaveChangesAsync();

        // Check if all tokens for this instance are consumed
        var allTokens = await _dbContext.WorkflowTokens
            .Where(t => t.InstanceId == instance.Id)
            .ToListAsync();

        var hasActiveTokens = allTokens.Any(t => t.Status == TokenStatus.Active);

        if (!hasActiveTokens)
        {
            instance.Status = InstanceStatus.Completed;
            instance.CompletedAt = DateTime.UtcNow;
            await _dbContext.SaveChangesAsync();
        }
    }

    private async Task ProcessApprovalNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var config = ParseConfig(node.Config);

        // Parse assignee
        var assigneeRule = GetString(config, "assigneeRule");

        // Create workflow task
        var task = new WorkflowTask
        {
            Id = Guid.NewGuid(),
            InstanceId = instance.Id,
            TokenId = token.Id,
            NodeId = node.Id,
            Type = TaskType.Approval,
            Status = TaskStatus.Pending,
        };

        if (assigneeRule is not null)
        {
            if (assigneeRule.StartsWith("user:"))
            {
                var userIdStr = assigneeRule["user:".Length..];
                task.AssigneeId = Guid.TryParse(userIdStr, out var userId) ? userId : null;
            }
            else if (assigneeRule.StartsWith("role:"))
            {
                task.AssigneeRole = assigneeRule["role:".Length..];
            }
        }

        _dbContext.WorkflowTasks.Add(task);

        // Execute onEnter actions
        await ExecuteActionsSafely(node, "onEnter", instance.Id);

        await _dbContext.SaveChangesAsync();

        // Token stays active - waiting for approval
    }

    private async Task ProcessCcNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var config = ParseConfig(node.Config);

        // Parse recipients
        var recipients = GetArray(config, "recipients");

        // Create CC tasks for each recipient
        foreach (var recipient in recipients)
        {
            var ccTask = new WorkflowTask
            {
                Id = Guid.NewGuid(),
                InstanceId = instance.Id,
                TokenId = token.Id,
                NodeId = node.Id,
                AssigneeId = Guid.TryParse(recipient, out var rcpId) ? rcpId : null,
                Type = TaskType.Cc,
                Status = TaskStatus.Pending,
            };
            _dbContext.WorkflowTasks.Add(ccTask);
        }

        // Execute onEnter actions
        await ExecuteActionsSafely(node, "onEnter", instance.Id);

        // Consume current token
        token.Status = TokenStatus.Consumed;
        token.CompletedAt = DateTime.UtcNow;

        // Find outgoing edge and create new token
        var edges = await GetOutgoingEdgesAsync(node.Id);
        var edge = edges.FirstOrDefault();

        if (edge is not null)
        {
            var newToken = new WorkflowToken
            {
                Id = Guid.NewGuid(),
                InstanceId = instance.Id,
                NodeId = edge.TargetNodeId,
                Status = TokenStatus.Active,
            };
            _dbContext.WorkflowTokens.Add(newToken);

            await _dbContext.SaveChangesAsync();

            // Process the target node
            var targetNode = await FindNodeAsync(edge.TargetNodeId);
            if (targetNode is not null)
            {
                await ProcessNodeAsync(instance, newToken, targetNode);
            }
        }
        else
        {
            await _dbContext.SaveChangesAsync();
        }
    }

    private async Task ProcessConditionNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var edges = await GetOutgoingEdgesAsync(node.Id);
        var variables = ParseVariables(instance.Variables);

        // Sort edges by order, then evaluate conditions
        var orderedEdges = edges.OrderBy(e => e.Order).ToList();

        foreach (var edge in orderedEdges)
        {
            if (_conditionEvaluator.Evaluate(edge.Condition, variables))
            {
                // Found matching branch - consume current token
                token.Status = TokenStatus.Consumed;
                token.CompletedAt = DateTime.UtcNow;

                // Create new token at target
                var newToken = new WorkflowToken
                {
                    Id = Guid.NewGuid(),
                    InstanceId = instance.Id,
                    NodeId = edge.TargetNodeId,
                    Status = TokenStatus.Active,
                };
                _dbContext.WorkflowTokens.Add(newToken);

                await _dbContext.SaveChangesAsync();

                // Process the target node
                var targetNode = await FindNodeAsync(edge.TargetNodeId);
                if (targetNode is not null)
                {
                    await ProcessNodeAsync(instance, newToken, targetNode);
                }

                return;
            }
        }

        // No matching branch found
        throw new BusinessException("No condition branch matched");
    }

    private async Task ProcessParallelNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var outgoingEdges = await GetOutgoingEdgesAsync(node.Id);
        var incomingEdges = await GetIncomingEdgesAsync(node.Id);

        if (outgoingEdges.Count > 1)
        {
            // Fork behavior: create one token per outgoing edge
            token.Status = TokenStatus.Consumed;
            token.CompletedAt = DateTime.UtcNow;

            await _dbContext.SaveChangesAsync();

            foreach (var edge in outgoingEdges)
            {
                var newToken = new WorkflowToken
                {
                    Id = Guid.NewGuid(),
                    InstanceId = instance.Id,
                    NodeId = edge.TargetNodeId,
                    Status = TokenStatus.Active,
                };
                _dbContext.WorkflowTokens.Add(newToken);

                // Process each target node
                var targetNode = await FindNodeAsync(edge.TargetNodeId);
                if (targetNode is not null)
                {
                    await _dbContext.SaveChangesAsync();
                    await ProcessNodeAsync(instance, newToken, targetNode);
                }
            }
        }
        else if (incomingEdges.Count > 1)
        {
            // Join behavior: check if all tokens have arrived
            var tokensAtNode = await _dbContext.WorkflowTokens
                .Where(t => t.InstanceId == instance.Id
                         && t.NodeId == node.Id
                         && t.Status == TokenStatus.Active)
                .ToListAsync();

            if (tokensAtNode.Count >= incomingEdges.Count)
            {
                // All tokens arrived - consume all and create one new token
                foreach (var t in tokensAtNode)
                {
                    t.Status = TokenStatus.Consumed;
                    t.CompletedAt = DateTime.UtcNow;
                }

                // Find outgoing edge
                var edge = outgoingEdges.FirstOrDefault();
                if (edge is not null)
                {
                    var newToken = new WorkflowToken
                    {
                        Id = Guid.NewGuid(),
                        InstanceId = instance.Id,
                        NodeId = edge.TargetNodeId,
                        Status = TokenStatus.Active,
                    };
                    _dbContext.WorkflowTokens.Add(newToken);

                    await _dbContext.SaveChangesAsync();

                    var targetNode = await FindNodeAsync(edge.TargetNodeId);
                    if (targetNode is not null)
                    {
                        await ProcessNodeAsync(instance, newToken, targetNode);
                    }
                }
                else
                {
                    await _dbContext.SaveChangesAsync();
                }
            }
            else
            {
                // Not all tokens arrived yet - wait
                await _dbContext.SaveChangesAsync();
            }
        }
        else
        {
            // Simple pass-through (single in, single out)
            token.Status = TokenStatus.Consumed;
            token.CompletedAt = DateTime.UtcNow;

            var edge = outgoingEdges.FirstOrDefault();
            if (edge is not null)
            {
                var newToken = new WorkflowToken
                {
                    Id = Guid.NewGuid(),
                    InstanceId = instance.Id,
                    NodeId = edge.TargetNodeId,
                    Status = TokenStatus.Active,
                };
                _dbContext.WorkflowTokens.Add(newToken);

                await _dbContext.SaveChangesAsync();

                var targetNode = await FindNodeAsync(edge.TargetNodeId);
                if (targetNode is not null)
                {
                    await ProcessNodeAsync(instance, newToken, targetNode);
                }
            }
            else
            {
                await _dbContext.SaveChangesAsync();
            }
        }
    }

    private async Task ProcessSubProcessNodeAsync(WorkflowInstance instance, WorkflowToken token, WorkflowNode node)
    {
        var config = ParseConfig(node.Config);
        var definitionIdStr = GetString(config, "definitionId");

        if (definitionIdStr is null)
            throw new BusinessException("SubProcess node must specify a definitionId");

        if (!Guid.TryParse(definitionIdStr, out var definitionId))
            throw new BusinessException("SubProcess definitionId must be a valid GUID");

        // Create child instance
        var childInstance = new WorkflowInstance
        {
            Id = Guid.NewGuid(),
            DefinitionId = definitionId,
            Status = InstanceStatus.Running,
            Variables = "{}",
            ParentInstanceId = instance.Id,
            ParentTokenId = token.Id,
        };
        _dbContext.WorkflowInstances.Add(childInstance);

        await _dbContext.SaveChangesAsync();

        // Token stays active at the sub-process node, waiting for child completion
    }

    // ============================================================
    // Helper Methods
    // ============================================================

    private async Task<WorkflowNode?> FindNodeAsync(Guid nodeId)
    {
        return await _dbContext.WorkflowNodes.FindAsync(nodeId);
    }

    private async Task<List<WorkflowEdge>> GetOutgoingEdgesAsync(Guid nodeId)
    {
        return await _dbContext.WorkflowEdges
            .Where(e => e.SourceNodeId == nodeId)
            .ToListAsync();
    }

    private async Task<List<WorkflowEdge>> GetIncomingEdgesAsync(Guid nodeId)
    {
        return await _dbContext.WorkflowEdges
            .Where(e => e.TargetNodeId == nodeId)
            .ToListAsync();
    }

    private Dictionary<string, object> ParseVariables(string? variablesJson)
    {
        if (string.IsNullOrWhiteSpace(variablesJson))
            return new();

        try
        {
            var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(variablesJson);
            if (dict is null)
                return new();

            var result = new Dictionary<string, object>();
            foreach (var kvp in dict)
            {
                result[kvp.Key] = kvp.Value.ValueKind switch
                {
                    JsonValueKind.Number => kvp.Value.TryGetDecimal(out var d) ? (object)d : kvp.Value.GetRawText(),
                    JsonValueKind.String => kvp.Value.GetString() ?? string.Empty,
                    JsonValueKind.True => true,
                    JsonValueKind.False => false,
                    _ => kvp.Value.GetRawText()
                };
            }
            return result;
        }
        catch (JsonException)
        {
            return new();
        }
    }

    private static Dictionary<string, JsonElement> ParseConfig(string? config)
    {
        if (string.IsNullOrWhiteSpace(config))
            return new();

        try
        {
            return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(config) ?? new();
        }
        catch (JsonException)
        {
            return new();
        }
    }

    private static string? GetString(Dictionary<string, JsonElement> config, string key)
    {
        if (config.TryGetValue(key, out var element) && element.ValueKind == JsonValueKind.String)
            return element.GetString();
        return null;
    }

    private static List<string> GetArray(Dictionary<string, JsonElement> config, string key)
    {
        if (!config.TryGetValue(key, out var element) || element.ValueKind != JsonValueKind.Array)
            return new();

        return element.EnumerateArray()
            .Where(e => e.ValueKind == JsonValueKind.String)
            .Select(e => e.GetString() ?? string.Empty)
            .ToList();
    }

    private async Task ExecuteActionsSafely(WorkflowNode node, string hook, Guid instanceId, string? result = null)
    {
        var config = ParseConfig(node.Config);
        if (!config.TryGetValue(hook, out var actionElement))
            return;

        string? actionName = actionElement.ValueKind == JsonValueKind.String
            ? actionElement.GetString()
            : null;

        if (actionName is null)
            return;

        try
        {
            var action = _serviceProvider.GetService(typeof(INodeAction)) as INodeAction;
            if (action is null)
                return;

            var instance = await _dbContext.WorkflowInstances.FindAsync(instanceId);
            var variables = instance is not null
                ? ParseVariables(instance.Variables)
                : new Dictionary<string, object>();

            var context = new NodeActionContext
            {
                Instance = instance!,
                Node = node,
                Variables = variables,
                Result = result,
            };

            await action.ExecuteAsync(context);
        }
        catch
        {
            // Action failure does not block token propagation
        }
    }
}
