<?php

namespace Modules\Workflows\Services;

use PDO;
use Exception;
use Modules\Workflows\Services\Actions\ActionInterface;

/**
 * Engine de execução de workflows
 */
class WorkflowEngine {
    private PDO $pdo;
    private string $tenantId;
    private array $actionRegistry = [];

    public function __construct(string $tenantId) {
        $this->tenantId = $tenantId;
        $this->pdo = \Gestoreasy\Core\Tenant\ConnectionManager::forTenant($tenantId);
        $this->registerDefaultActions();
    }

    /**
     * Registra ações padrão
     */
    private function registerDefaultActions(): void {
        $this->registerAction('send_email', \Modules\Workflows\Services\Actions\SendEmailAction::class);
        $this->registerAction('create_record', \Modules\Workflows\Services\Actions\CreateRecordAction::class);
        $this->registerAction('update_record', \Modules\Workflows\Services\Actions\UpdateRecordAction::class);
        $this->registerAction('notify', \Modules\Workflows\Services\Actions\NotifyAction::class);
        $this->registerAction('approve', \Modules\Workflows\Services\Actions\ApproveAction::class);
    }

    /**
     * Registra uma ação customizada
     */
    public function registerAction(string $type, string $actionClass): void {
        if (!is_subclass_of($actionClass, ActionInterface::class)) {
            throw new Exception("Action class must implement ActionInterface: {$actionClass}");
        }
        $this->actionRegistry[$type] = $actionClass;
    }

    /**
     * Executa um workflow
     */
    public function execute(int $workflowId, array $inputData = []): array {
        // Buscar workflow
        $stmt = $this->pdo->prepare('
            SELECT * FROM workflows 
            WHERE id = ? AND tenant_id = ? AND status = "ativo" AND ativo = TRUE
        ');
        $stmt->execute([$workflowId, $this->tenantId]);
        $workflow = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$workflow) {
            throw new Exception('Workflow não encontrado ou inativo');
        }

        // Criar execução
        $executionId = $this->createExecution($workflowId, $inputData);

        try {
            // Executar workflow
            $definition = json_decode($workflow['definition'], true);
            $output = $this->executeWorkflow($definition, $inputData, $executionId);

            // Marcar execução como sucesso
            $this->updateExecution($executionId, 'sucesso', $output);

            // Atualizar estatísticas do workflow
            $this->updateWorkflowStats($workflowId, true);

            return [
                'execution_id' => $executionId,
                'status' => 'sucesso',
                'output' => $output
            ];
        } catch (Exception $e) {
            // Marcar execução como erro
            $this->updateExecution($executionId, 'erro', null, $e->getMessage());

            // Atualizar estatísticas do workflow
            $this->updateWorkflowStats($workflowId, false);

            throw $e;
        }
    }

    /**
     * Executa a definição do workflow
     */
    private function executeWorkflow(array $definition, array $inputData, int $executionId): array {
        $nodes = $definition['nodes'] ?? [];
        $edges = $definition['edges'] ?? [];
        $startNode = $this->findStartNode($nodes);

        if (!$startNode) {
            throw new Exception('Nenhum nó inicial encontrado no workflow');
        }

        $context = [
            'input' => $inputData,
            'variables' => $inputData,
            'execution_id' => $executionId
        ];

        return $this->executeNode($startNode, $nodes, $edges, $context, $executionId);
    }

    /**
     * Encontra o nó inicial do workflow
     */
    private function findStartNode(array $nodes): ?array {
        foreach ($nodes as $node) {
            if (($node['type'] ?? '') === 'start' || ($node['type'] ?? '') === 'trigger') {
                return $node;
            }
        }
        // Se não encontrar, retorna o primeiro nó
        return !empty($nodes) ? $nodes[0] : null;
    }

    /**
     * Executa um nó do workflow
     */
    private function executeNode(array $node, array $allNodes, array $edges, array &$context, int $executionId): array {
        $nodeId = $node['id'] ?? '';
        $nodeType = $node['type'] ?? '';

        // Log início do node
        $logId = $this->logNodeStart($executionId, $nodeId, $nodeType, $context);

        try {
            $output = null;

            switch ($nodeType) {
                case 'start':
                case 'trigger':
                    // Nó inicial, apenas passa os dados
                    $output = $context['input'];
                    break;

                case 'action':
                    // Executar ação
                    $output = $this->executeAction($node, $context, $executionId);
                    
                    // Se for ação de aprovação e ainda não aprovada, pausar workflow
                    $actionType = $node['action'] ?? '';
                    if ($actionType === 'approve' && isset($output['status']) && $output['status'] === 'pending') {
                        // Workflow pausado, não continuar execução
                        $this->logNodeComplete($logId, 'pendente', $output);
                        return $output;
                    }
                    break;

                case 'condition':
                    // Avaliar condição
                    $output = $this->evaluateCondition($node, $context);
                    break;

                case 'delay':
                    // Aguardar tempo
                    $this->executeDelay($node);
                    $output = $context['variables'];
                    break;

                default:
                    throw new Exception("Tipo de nó não suportado: {$nodeType}");
            }

            // Atualizar contexto
            if (is_array($output)) {
                $context['variables'] = array_merge($context['variables'], $output);
            }

            // Log sucesso do node
            $this->logNodeComplete($logId, 'sucesso', $output);

            // Executar próximos nós
            $nextNodes = $this->getNextNodes($nodeId, $edges, $output, $nodeType);
            foreach ($nextNodes as $nextNodeId) {
                $nextNode = $this->findNodeById($nextNodeId, $allNodes);
                if ($nextNode) {
                    $this->executeNode($nextNode, $allNodes, $edges, $context, $executionId);
                }
            }

            return $output ?? $context['variables'];
        } catch (Exception $e) {
            // Log erro do node
            $this->logNodeComplete($logId, 'erro', null, $e->getMessage());
            throw $e;
        }
    }

    /**
     * Executa uma ação
     */
    private function executeAction(array $node, array $context, int $executionId): array {
        $actionType = $node['action'] ?? '';
        $actionConfig = $node['config'] ?? [];
        $nodeId = $node['id'] ?? '';

        if (!isset($this->actionRegistry[$actionType])) {
            throw new Exception("Ação não registrada: {$actionType}");
        }

        // Adicionar informações do workflow ao contexto
        $context['execution_id'] = $executionId;
        $context['workflow_id'] = $this->getWorkflowId($executionId);
        $context['node_id'] = $nodeId;
        
        // Adicionar workflow_id e node_id à configuração se for aprovação
        if ($actionType === 'approve') {
            $actionConfig['workflow_id'] = $context['workflow_id'];
            $actionConfig['node_id'] = $nodeId;
        }

        $actionClass = $this->actionRegistry[$actionType];
        $action = new $actionClass($this->tenantId);

        return $action->execute($actionConfig, $context);
    }

    /**
     * Obtém workflow ID de uma execução
     */
    private function getWorkflowId(int $executionId): ?int {
        $stmt = $this->pdo->prepare('SELECT workflow_id FROM workflow_executions WHERE id = ?');
        $stmt->execute([$executionId]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result ? (int)$result['workflow_id'] : null;
    }

    /**
     * Avalia uma condição
     */
    private function evaluateCondition(array $node, array $context): bool {
        $condition = $node['condition'] ?? [];
        $field = $condition['field'] ?? '';
        $operator = $condition['operator'] ?? '==';
        $value = $condition['value'] ?? null;

        $fieldValue = $this->getFieldValue($field, $context);

        return $this->compare($fieldValue, $operator, $value);
    }

    /**
     * Obtém valor de um campo do contexto
     */
    private function getFieldValue(string $field, array $context) {
        // Suporta notação de ponto (ex: "variables.nome")
        $parts = explode('.', $field);
        $value = $context;

        foreach ($parts as $part) {
            if (isset($value[$part])) {
                $value = $value[$part];
            } else {
                return null;
            }
        }

        return $value;
    }

    /**
     * Compara valores
     */
    private function compare($fieldValue, string $operator, $value): bool {
        return match($operator) {
            '==' => $fieldValue == $value,
            '!=' => $fieldValue != $value,
            '>' => $fieldValue > $value,
            '>=' => $fieldValue >= $value,
            '<' => $fieldValue < $value,
            '<=' => $fieldValue <= $value,
            'contains' => str_contains((string)$fieldValue, (string)$value),
            'not_contains' => !str_contains((string)$fieldValue, (string)$value),
            'in' => in_array($fieldValue, (array)$value),
            'not_in' => !in_array($fieldValue, (array)$value),
            default => false
        };
    }

    /**
     * Executa delay
     */
    private function executeDelay(array $node): void {
        $seconds = (int)($node['seconds'] ?? 0);
        if ($seconds > 0) {
            sleep($seconds);
        }
    }

    /**
     * Obtém próximos nós baseado nas arestas
     */
    private function getNextNodes(string $nodeId, array $edges, $conditionResult, string $nodeType): array {
        $nextNodes = [];

        foreach ($edges as $edge) {
            $source = $edge['source'] ?? '';
            $target = $edge['target'] ?? '';
            $condition = $edge['condition'] ?? null;

            if ($source === $nodeId) {
                // Se for condição, verificar se deve seguir esta aresta
                if ($nodeType === 'condition') {
                    if ($condition === 'true' && $conditionResult) {
                        $nextNodes[] = $target;
                    } elseif ($condition === 'false' && !$conditionResult) {
                        $nextNodes[] = $target;
                    }
                } else {
                    // Sem condição, sempre segue
                    $nextNodes[] = $target;
                }
            }
        }

        return $nextNodes;
    }

    /**
     * Encontra nó por ID
     */
    private function findNodeById(string $nodeId, array $nodes): ?array {
        foreach ($nodes as $node) {
            if (($node['id'] ?? '') === $nodeId) {
                return $node;
            }
        }
        return null;
    }

    /**
     * Cria execução
     */
    private function createExecution(int $workflowId, array $inputData): int {
        $stmt = $this->pdo->prepare('
            INSERT INTO workflow_executions (tenant_id, workflow_id, status, input_data, started_at)
            VALUES (?, ?, "executando", ?, NOW())
        ');
        $stmt->execute([$this->tenantId, $workflowId, json_encode($inputData)]);
        return (int)$this->pdo->lastInsertId();
    }

    /**
     * Atualiza execução
     */
    private function updateExecution(int $executionId, string $status, ?array $output, ?string $error = null): void {
        $stmt = $this->pdo->prepare('
            UPDATE workflow_executions
            SET status = ?,
                output_data = ?,
                error_message = ?,
                completed_at = NOW(),
                duration_ms = TIMESTAMPDIFF(MICROSECOND, started_at, NOW()) / 1000
            WHERE id = ? AND tenant_id = ?
        ');
        $stmt->execute([
            $status,
            $output ? json_encode($output) : null,
            $error,
            $executionId,
            $this->tenantId
        ]);
    }

    /**
     * Log início do node
     */
    private function logNodeStart(int $executionId, string $nodeId, string $nodeType, array $context): int {
        $stmt = $this->pdo->prepare('
            INSERT INTO workflow_execution_logs 
            (tenant_id, execution_id, node_id, node_type, status, input_data, started_at)
            VALUES (?, ?, ?, ?, "executando", ?, NOW())
        ');
        $stmt->execute([
            $this->tenantId,
            $executionId,
            $nodeId,
            $nodeType,
            json_encode($context)
        ]);
        return (int)$this->pdo->lastInsertId();
    }

    /**
     * Log conclusão do node
     */
    private function logNodeComplete(int $logId, string $status, ?array $output, ?string $error = null): void {
        $stmt = $this->pdo->prepare('
            UPDATE workflow_execution_logs
            SET status = ?,
                output_data = ?,
                error_message = ?,
                completed_at = NOW(),
                duration_ms = TIMESTAMPDIFF(MICROSECOND, started_at, NOW()) / 1000
            WHERE id = ?
        ');
        $stmt->execute([
            $status,
            $output ? json_encode($output) : null,
            $error,
            $logId
        ]);
    }

    /**
     * Atualiza estatísticas do workflow
     */
    private function updateWorkflowStats(int $workflowId, bool $success): void {
        $field = $success ? 'execucoes_sucesso' : 'execucoes_erro';
        $stmt = $this->pdo->prepare("
            UPDATE workflows
            SET execucoes_total = execucoes_total + 1,
                {$field} = {$field} + 1,
                ultima_execucao = NOW()
            WHERE id = ? AND tenant_id = ?
        ");
        $stmt->execute([$workflowId, $this->tenantId]);
    }
}

