<?php
// Cache-buster: 1720638682
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/lib/WorkflowExceptions.php';

class WorkflowEngine {

    private $pdo;

    public function __construct() {
        $this->pdo = db();
    }

        public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null, ?int $meetingFilterGroupId = null, ?string $meetingFilterDatetime = null): array {
        // 1. Base query for people
        $sql_people = "SELECT p.*, bg.name as bni_group_name FROM people p LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id";
        $params = [];
        $where_clauses = [];

        // 2. Add filter conditions
        if ($searchTerm) {
            $where_clauses[] = "(p.first_name LIKE :search OR p.last_name LIKE :search OR p.company_name LIKE :search OR p.email LIKE :search)";
            $params[':search'] = '%' . $searchTerm . '%';
        }

        if ($groupId) {
            $where_clauses[] = "p.bni_group_id = :group_id";
            $params[':group_id'] = $groupId;
        }
        
        if ($activeProcessDefinitionId) {
            $terminal_statuses = ['positive', 'negative', 'completed', 'error', 'inactive'];
            $in_clause = implode(',', array_map([$this->pdo, 'quote'], $terminal_statuses));
            
            $sql_people .= " INNER JOIN process_instances pi ON p.id = pi.person_id";
            $where_clauses[] = "pi.process_definition_id = :active_process_id AND (pi.current_status IS NOT NULL AND pi.current_status NOT IN ($in_clause))";
            $params[':active_process_id'] = $activeProcessDefinitionId;
        }

        if ($meetingFilterGroupId && $meetingFilterDatetime) {
            $meetingId = $this->getOrCreateMeeting($meetingFilterGroupId, $meetingFilterDatetime);
            $sql_people .= " INNER JOIN meeting_attendance ma ON p.id = ma.person_id";
            $where_clauses[] = "ma.meeting_id = :meeting_id";
            $where_clauses[] = "ma.attendance_status IN ('present', 'absent', 'substitute')";
            $params[':meeting_id'] = $meetingId;
        }

        if (!empty($where_clauses)) {
            $sql_people .= " WHERE " . implode(" AND ", $where_clauses);
        }

        $sql_people .= " ORDER BY p.last_name, p.first_name";

        // 3. Execute query to get filtered people
        $stmt_people = $this->pdo->prepare($sql_people);
        $stmt_people->execute($params);
        $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);

        // 4. Fetch all process definitions with their JSON
        $stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name");
        $stmt_defs->execute();
        $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
        
        $definitions = [];
        $definition_map = [];
        foreach ($process_definitions_raw as $def) {
            $definitions[$def['id']] = [
                'id' => $def['id'],
                'name' => $def['name'],
                'is_active' => $def['is_active']
            ];
            $definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null;
        }

        // 5. Fetch instances ONLY for the filtered people
        $instances = [];
        $person_ids = array_column($people, 'id');
        if (!empty($person_ids)) {
            $placeholders = implode(',', array_fill(0, count($person_ids), '?'));
            $stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances WHERE person_id IN ($placeholders)");
            $stmt_instances->execute($person_ids);
            $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
        
            foreach ($instances_data as $instance) {
                $enriched_instance = $instance;
                $def_id = $instance['process_definition_id'];
                $node_id = $instance['current_node_id'];
    
                $definition = $definition_map[$def_id] ?? null;
    
                if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') {
                    $tasks = $definition['tasks'] ?? [];
                    $instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
                    $totalTasks = count($tasks);
                    $completedTasks = 0;
                    if(is_array($instanceData)) {
                        foreach ($tasks as $task) {
                            if (!empty($instanceData[$task['code']])) {
                                $completedTasks++;
                            }
                        }
                    }
    
                    if ($totalTasks > 0 && $completedTasks === $totalTasks) {
                        $status = 'completed';
                    } elseif ($completedTasks > 0) {
                        $status = 'in_progress';
                    } else {
                        $status = 'inactive';
                    }
                    $enriched_instance['computed_status'] = $status;
                    $enriched_instance['computed_reason'] = "$completedTasks/$totalTasks completed";
                    $enriched_instance['computed_next_step'] = ''; 
                } else if ($definition && isset($definition['nodes'][$node_id])) {
                    $node_info = $definition['nodes'][$node_id];
                    $enriched_instance['computed_status'] = $node_info['ui_hints']['status'] ?? $instance['current_status'];
                    $enriched_instance['computed_reason'] = $node_info['ui_hints']['reason'] ?? $instance['current_reason'];
                    $enriched_instance['computed_next_step'] = $node_info['ui_hints']['next_step'] ?? $instance['suggested_next_step'];
                } else {
                    $enriched_instance['computed_status'] = $instance['current_status'];
                    $enriched_instance['computed_reason'] = $instance['current_reason'];
                    $enriched_instance['computed_next_step'] = $instance['suggested_next_step'];
                }
                
                $instances[$instance['person_id']][$def_id] = $enriched_instance;
            }
        }

        // 6. Fetch ancillary data
        $stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order");
        $all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC);

        $stmt_person_functions = $this->pdo->query("SELECT user_id, function_id FROM user_functions");
        $person_functions_map = [];
        while ($row = $stmt_person_functions->fetch(PDO::FETCH_ASSOC)) {
            $person_functions_map[$row['user_id']][] = $row['function_id'];
        }

        $stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name");
        $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);

        // 7. Fetch Spotkania columns (upcoming meetings)
        $today = date('Y-m-d H:i:s');
        $stmt_meetings = $this->pdo->prepare("
            WITH RankedMeetings AS (
                SELECT
                    bg.id as group_id,
                    bg.name as group_name,
                    ce.start_datetime,
                    ROW_NUMBER() OVER(PARTITION BY bg.id ORDER BY ce.start_datetime) as rn
                FROM bni_groups bg
                JOIN calendar_event_groups ceg ON bg.id = ceg.bni_group_id
                JOIN calendar_events ce ON ceg.calendar_event_id = ce.id
                WHERE ce.start_datetime >= :today
            )
            SELECT group_id, group_name, start_datetime
            FROM RankedMeetings
            WHERE rn <= 3
            ORDER BY group_id, start_datetime;
        ");
        $stmt_meetings->execute(['today' => $today]);
        $upcoming_meetings_flat = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC);

        $spotkania_cols = [];
        foreach ($upcoming_meetings_flat as $meeting) {
            $spotkania_cols[$meeting['group_id']]['group_id'] = $meeting['group_id'];
            $spotkania_cols[$meeting['group_id']]['group_name'] = $meeting['group_name'];
            $spotkania_cols[$meeting['group_id']]['meetings'][] = $meeting['start_datetime'];
        }


        return [
            'people' => $people,
            'definitions' => array_values($definitions),
            'instances' => $instances,
            'all_functions' => $all_functions,
            'person_functions_map' => $person_functions_map,
            'bni_groups' => $bni_groups,
            'spotkania_cols' => $spotkania_cols, // Add this to the return array
        ];
    }

        public function startProcess(string $processCode, int $personId, int $userId): int {
        $this->pdo->beginTransaction();
        try {
            // 1. Find active process definition by code.
            $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND is_active = 1");
            $stmt_def->execute([$processCode]);
            $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);

            if (!$definition) {
                // If no process definition is found, check if there is a definition for a checklist
                $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
                $stmt_def->execute([$processCode]);
                $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);

                if (!$definition) {
                    throw new WorkflowNotFoundException("Process definition with code or id '$processCode' not found.");
                }

                $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
                if (empty($definition_json) || $definition_json['type'] !== 'checklist') {
                    throw new WorkflowNotAllowedException("Process definition with code '$processCode' not found or not a checklist.");
                }

                // For checklists, there's no start_node_id, so we can proceed with instance creation
                $startNodeId = null;

            } else {
                $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
                if (empty($definition_json) || !isset($definition_json['start_node_id'])) {
                    throw new WorkflowRuleFailedException("Process definition is missing start_node_id.");
                }
                $startNodeId = $definition_json['start_node_id'];
            }

            // 2. Create a new process instance.
            $stmt_insert = $this->pdo->prepare(
                "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, last_activity_at) VALUES (?, ?, ?, 'in_progress', NOW())"
            );
            $stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
            $instanceId = $this->pdo->lastInsertId();

            // 3. Create a system event for process start.
            $this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId);

            $this->pdo->commit();
            return (int)$instanceId;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    public function getProcessState(int $instanceId): ?array {
        $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
        $stmt->execute([$instanceId]);
        $instance = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$instance) {
            return null;
        }

        $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
        $stmt_def->execute([$instance['process_definition_id']]);
        $definition_json = $stmt_def->fetchColumn();
        $definition = !empty($definition_json) ? json_decode($definition_json, true) : [];

        $currentNodeId = $instance['current_node_id'];
        $nodeInfo = $definition['nodes'][$currentNodeId] ?? null;

        return [
            'instance' => $instance,
            'definition' => $definition,
            'currentNode' => $nodeInfo,
        ];
    }

        public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): array {
        $this->pdo->beginTransaction();
        try {
            $state = $this->getProcessState($instanceId);
            if (!$state) {
                throw new WorkflowNotFoundException("Process instance not found.");
            }

            $instance = $state['instance'];
            $definition = $state['definition'];
            $currentNodeId = $instance['current_node_id'];

            $transition = null;
            foreach ($definition['transitions'] as $t) {
                if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) {
                    // For user-triggered transitions, we ignore the condition here.
                    // The UI should prevent showing buttons for transitions whose data-based conditions aren't met.
                    $transition = $t;
                    break;
                }
            }

            if (!$transition) {
                throw new WorkflowNotAllowedException("Transition not found or not allowed from the current node.");
            }

            // Apply the initial, user-triggered transition
            $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $transition, $inputPayload, $userId);
            $instance['current_node_id'] = $newNodeId; // Update instance state for the loop

            // Loop for automatic transitions (router nodes)
            for ($i = 0; $i < 10; $i++) { // Max 10 auto-steps to prevent infinite loops
                $autoTransition = $this->findAutomaticTransition($instance, $definition);
                if ($autoTransition) {
                    // Automatic transitions have no user payload
                    $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $autoTransition, [], $userId);
                    $instance['current_node_id'] = $newNodeId; // Update for next iteration
                } else {
                    break; // No more automatic transitions found
                }
            }
            
            $this->pdo->commit();

            // Refetch the final state of the instance to return the correct status
            $finalState = $this->getProcessState($instanceId)['instance'];
            $finalNodeInfo = $definition['nodes'][$finalState['current_node_id']] ?? null;

            return [
                'instanceId' => $instanceId,
                'currentNodeId' => $finalState['current_node_id'],
                'currentStatus' => $finalNodeInfo['ui_hints']['status'] ?? $finalState['current_status'],
                'currentReason' => $finalNodeInfo['ui_hints']['reason'] ?? $finalState['current_reason'],
                'suggestedNextStep' => $finalNodeInfo['ui_hints']['next_step'] ?? $finalState['suggested_next_step'],
                'lastActivityAt' => $finalState['last_activity_at'],
            ];

        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    private function applySingleTransition(int $instanceId, array &$instance, array $definition, array $transition, array $inputPayload, int $userId): string 
    {
        $newNodeId = $transition['to'];
        $newNodeInfo = $definition['nodes'][$newNodeId] ?? null;

        $newStatus = $newNodeInfo['ui_hints']['status'] ?? 'in_progress';
        $newReason = $newNodeInfo['ui_hints']['reason'] ?? '';
        $newNextStep = $newNodeInfo['ui_hints']['next_step'] ?? '';

        $stmt_update = $this->pdo->prepare(
            "UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, last_activity_at = NOW() WHERE id = ?"
        );
        $stmt_update->execute([
            $newNodeId,
            $newStatus,
            $newReason,
            $newNextStep,
            $instanceId
        ]);

        $message = $inputPayload['message'] ?? $transition['name'];
        $this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId);

        if (isset($transition['actions'])) {
            foreach ($transition['actions'] as $action) {
                if ($action['type'] === 'start_process') {
                    $this->executeStartProcessAction($instance['person_id'], $action, $userId);
                } elseif ($action['type'] === 'set_data') {
                    // Pass the instance by reference to be updated with new data
                    $this->executeSetDataAction($instanceId, $instance, $action, $inputPayload);
                }
            }
        }

        return $newNodeId;
    }

    private function findAutomaticTransition(array $instance, array $definition): ?array
    {
        $currentNodeId = $instance['current_node_id'];
        foreach ($definition['transitions'] as $transition) {
            if ($transition['from'] === $currentNodeId) {
                // An automatic transition MUST have a condition.
                if (isset($transition['condition'])) {
                    if ($this->checkTransitionCondition($transition, $instance)) {
                        return $transition;
                    }
                }
            }
        }
        return null;
    }

    public function addNote(int $instanceId, string $message, int $userId): bool {
        $state = $this->getProcessState($instanceId);
        if (!$state) {
            throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
        }
        $currentNodeId = $state['instance']['current_node_id'];
        $payload = ['message' => $message];
        $this->addEvent($instanceId, 'note', $message, $currentNodeId, $payload, $userId);
        return true;
    }

    public function applyManualStatus(int $instanceId, string $status, string $reasonOrNote, int $userId): bool {
        $this->pdo->beginTransaction();
        try {
            $state = $this->getProcessState($instanceId);
            if (!$state) {
                throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
            }

            $stmt_update = $this->pdo->prepare(
                "UPDATE process_instances SET current_status = ?, current_reason = ?, last_activity_at = NOW() WHERE id = ?"
            );
            $stmt_update->execute([$status, $reasonOrNote, $instanceId]);

            $currentNodeId = $state['instance']['current_node_id'];
            $message = "Status manually set to '$status'.";
            if (!empty($reasonOrNote)) {
                $message .= " Reason: $reasonOrNote";
            }
            
            $this->addEvent($instanceId, 'manual_status_change', $message, $currentNodeId, ['status' => $status, 'reason' => $reasonOrNote], $userId);

            $this->pdo->commit();
            return true;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    public function bulkAddNotes(array $notes): array
    {
        $results = [];
        foreach ($notes as $note) {
            try {
                $this->addNote((int)$note['instance_id'], $note['message'], (int)$note['user_id']);
                $results[] = ['instance_id' => $note['instance_id'], 'success' => true];
            } catch (Exception $e) {
                $results[] = ['instance_id' => $note['instance_id'], 'success' => false, 'error' => $e->getMessage()];
            }
        }
        return $results;
    }

    public function bulkManualStatus(array $statuses): array
    {
        $results = [];
        foreach ($statuses as $status) {
            try {
                $this->applyManualStatus((int)$status['instance_id'], $status['status'], $status['reason'] ?? '', (int)$status['user_id']);
                $results[] = ['instance_id' => $status['instance_id'], 'success' => true];
            } catch (Exception $e) {
                $results[] = ['instance_id' => $status['instance_id'], 'success' => false, 'error' => $e->getMessage()];
            }
        }
        return $results;
    }

    public function updateChecklistStatus(int $instanceId, string $taskCode, bool $isChecked, int $userId): array
    {
        $this->pdo->beginTransaction();
        try {
            // Get current data_json
            $stmt = $this->pdo->prepare("SELECT data_json, process_definition_id FROM process_instances WHERE id = ?");
            $stmt->execute([$instanceId]);
            $instance = $stmt->fetch(PDO::FETCH_ASSOC);

            if (!$instance) {
                throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
            }

            $data = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];

            // Update the specific task status
            $data[$taskCode] = $isChecked;
            $newDataJson = json_encode($data);

            // Save new data_json and update timestamp
            $stmt = $this->pdo->prepare("UPDATE process_instances SET data_json = ?, last_activity_at = CURRENT_TIMESTAMP WHERE id = ?");
            $stmt->execute([$newDataJson, $instanceId]);

            // Add an event for the checklist update
            $message = "Checklist task '$taskCode' marked as " . ($isChecked ? 'complete' : 'incomplete') . ".";
            $this->addEvent($instanceId, 'checklist_update', $message, null, ['task' => $taskCode, 'checked' => $isChecked], $userId);

            // Calculate progress
            $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
            $stmt_def->execute([$instance['process_definition_id']]);
            $definitionJson = $stmt_def->fetchColumn();
            $definition = json_decode($definitionJson, true);
            $totalTasks = count($definition['tasks'] ?? []);
            $completedTasks = count(array_filter($data));
            
            $this->pdo->commit();

            return [
                'success' => true,
                'progress' => [
                    'completed' => $completedTasks,
                    'total' => $totalTasks
                ],
                'lastActivityAt' => date('d/m/y')
            ];

        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }


    private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void {
        $stmt = $this->pdo->prepare(
            "INSERT INTO process_events (process_instance_id, event_type, message, node_id, payload_json, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())"
        );
        $stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]);
    }

    public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {
        if (!is_int($processDefinitionId) || $processDefinitionId <= 0) {
            throw new InvalidArgumentException("processDefinitionId must be a positive integer.");
        }
        if (!is_int($personId) || $personId <= 0) {
            throw new InvalidArgumentException("personId must be a positive integer.");
        }

        $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
        $stmt->execute([$personId, $processDefinitionId]);
        $instance = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$instance) {
            $stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?");
            $stmt_def->execute([$processDefinitionId]);
            $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);

            if (!$definition) {
                throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found.");
            }

            if (empty($definition['is_active'])) {
                throw new WorkflowNotAllowedException("Process is not active and cannot be started.");
            }

            $eligibility = $this->checkEligibility($personId, $processDefinitionId, $context);
            if (!$eligibility['is_eligible']) {
                throw new WorkflowEligibilityException("Person is not eligible to start this process.", $eligibility['reasons']);
            }

            $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];

            $processCode = ($definition_json && isset($definition_json['type']) && $definition_json['type'] === 'checklist')
                           ? (string) $processDefinitionId
                           : $definition['code'];

            if ($processCode) {
                $instanceId = $this->startProcess($processCode, $personId, $userId);
                if ($instanceId) {
                    $stmt->execute([$personId, $processDefinitionId]);
                    $instance = $stmt->fetch(PDO::FETCH_ASSOC);
                }
            }
        }

        return $instance ?: null;
    }

    public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array {
        $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
        $stmt->execute([$personId, $processDefinitionId]);
        $instance = $stmt->fetch(PDO::FETCH_ASSOC);
        return $instance ?: null;
    }

    public function getEvents(int $instanceId): array {
        $stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.first_name, p.last_name FROM process_events pe JOIN people p ON pe.created_by = p.id WHERE pe.process_instance_id = ? ORDER BY pe.created_at DESC");
        $stmt_events->execute([$instanceId]);
        return $stmt_events->fetchAll(PDO::FETCH_ASSOC);
    }

    public function getAvailableTransitions(int $instanceId): array {
        $state = $this->getProcessState($instanceId);
        if (!$state) {
            return [];
        }

        $currentNodeId = $state['instance']['current_node_id'];
        $definition = $state['definition'];

        $transitions = [];
        if (isset($definition['transitions'])) {
            foreach ($definition['transitions'] as $t) {
                if ($t['from'] === $currentNodeId) {
                    $transitions[] = $t;
                }
            }
        }

        return $transitions;
    }

    public function getProcessDefinitionNodes(int $processDefinitionId): array {
        $stmt = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
        $stmt->execute([$processDefinitionId]);
        $json = $stmt->fetchColumn();

        if (!$json) {
            return [];
        }

        $definition = !empty($json) ? json_decode($json, true) : [];
        return $definition['nodes'] ?? [];
    }

    public function checkEligibility(int $personId, int $processDefinitionId, array $context = []): array {
        $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
        $stmt_def->execute([$processDefinitionId]);
        $definition_json = $stmt_def->fetchColumn();
        
        $definition = !empty($definition_json) ? json_decode($definition_json, true) : [];

        $reasons = [];
        if (empty($definition) || empty($definition['eligibility_rules'])) {
            return ['is_eligible' => true, 'reasons' => []];
        }

        foreach ($definition['eligibility_rules'] as $rule) {
            try {
                $params = $rule['params'] ?? $rule;
                switch ($rule['type']) {
                    case 'checkProcessCompletedRule':
                    case 'process_completed': // Backward compatibility
                        $this->checkProcessCompletedRule($personId, $params);
                        break;
                    case 'checkProcessDataRule':
                        $this->checkProcessDataRule($personId, $params);
                        break;
                    case 'deny_manual_start':
                        $this->checkDenyManualStartRule($context);
                        break;
                    case 'person_property_equals':
                        $this->checkPersonPropertyEqualsRule($personId, $params);
                        break;
                    // Add other rule types here
                }
            } catch (WorkflowNotAllowedException $e) {
                $reasons[] = $e->getMessage();
            }
        }

        return ['is_eligible' => empty($reasons), 'reasons' => $reasons];
    }

    private function checkDenyManualStartRule(array $context): void {
        if (!isset($context['source']) || $context['source'] !== 'chain') {
            throw new WorkflowNotAllowedException("This process can only be started automatically by another process.");
        }
    }

    private function checkPersonPropertyEqualsRule(int $personId, array $params): void {
        $stmt = $this->pdo->prepare("SELECT * FROM people WHERE id = ?");
        $stmt->execute([$personId]);
        $person = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$person) {
            throw new WorkflowNotAllowedException("Person not found.");
        }

        $property = $params['property'];
        $expectedValue = $params['value'];

        if (!isset($person[$property])) {
            throw new WorkflowNotAllowedException("Property '{$property}' not found on person.");
        }

        if ($person[$property] !== $expectedValue) {
            throw new WorkflowNotAllowedException("Person's property '{$property}' is not '{$expectedValue}'.");
        }
    }

    private function checkProcessCompletedRule(int $personId, array $params): void {
        $stmt = $this->pdo->prepare("\n            SELECT pi.id\n            FROM process_instances pi\n            JOIN process_definitions pd ON pi.process_definition_id = pd.id\n            WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?\n            ORDER BY pi.last_activity_at DESC\n            LIMIT 1\n        ");
        $stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
        $instance = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$instance) {
            throw new WorkflowNotAllowedException("Prerequisite process '{$params['process_code']}' not completed with status '{$params['expected_status']}'.");
        }
    }

    private function checkProcessDataRule(int $personId, array $params): void {
        $stmt = $this->pdo->prepare("
            SELECT pi.data_json
            FROM process_instances pi
            JOIN process_definitions pd ON pi.process_definition_id = pd.id
            WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?
            ORDER BY pi.last_activity_at DESC
            LIMIT 1
        ");
        $stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
        $data_json = $stmt->fetchColumn();

        if (!$data_json) {
            throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$params['process_code']}' not found with status '{$params['expected_status']}'.");
        }

        $data = json_decode($data_json, true);
        if (!is_array($data)) {
            $data = [];
        }

        foreach ($params['expected_data'] as $key => $expected_value) {
            if (!isset($data[$key]) || $data[$key] !== $expected_value) {
                throw new WorkflowNotAllowedException("Not eligible. Condition not met: '$key' is not '$expected_value'.");
            }
        }
    }

    private function checkTransitionCondition(array $transition, array $instanceData): bool
    {
        if (!isset($transition['condition'])) {
            // A transition without a condition is not automatic, but it is valid to pass through.
            // The calling context (findAutomaticTransition) will decide if this is an error.
            return true;
        }

        $condition = $transition['condition'];
        $data = isset($instanceData['data_json']) ? json_decode($instanceData['data_json'], true) : [];

        $field = $condition['field'] ?? null;
        $expectedValue = $condition['value'] ?? null;

        if ($field === null || $expectedValue === null) {
            // Malformed condition
            return false;
        }

        return isset($data[$field]) && $data[$field] === $expectedValue;
    }

    private function executeStartProcessAction(int $personId, array $action, int $userId): void {
        $stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE code = ?");
        $stmt->execute([$action['process_code']]);
        $processDefinitionId = $stmt->fetchColumn();

        if ($processDefinitionId) {
            $this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId, ['source' => 'chain']);
        }
    }

    private function executeSetDataAction(int $instanceId, array &$instance, array $action, array $payload): void {
        $dataJson = $instance['data_json'];
        $data = $dataJson ? json_decode($dataJson, true) : [];

        if (isset($action['params']['keys']) && is_array($action['params']['keys'])) {
            foreach ($action['params']['keys'] as $key) {
                if (array_key_exists($key, $payload)) {
                    $data[$key] = $payload[$key];
                }
            }
        }

        $newDataJson = json_encode($data);

        // Update the database
        $stmt_update = $this->pdo->prepare("UPDATE process_instances SET data_json = ? WHERE id = ?");
        $stmt_update->execute([$newDataJson, $instanceId]);

        // Also update the in-memory instance for the next step in the chain
        $instance['data_json'] = $newDataJson;
    }

    public function deleteInstance(int $instanceId): void {
        $this->pdo->beginTransaction();
        try {
            // Delete events
            $stmt_events = $this->pdo->prepare("DELETE FROM process_events WHERE process_instance_id = ?");
            $stmt_events->execute([$instanceId]);

            // Delete instance
            $stmt_instance = $this->pdo->prepare("DELETE FROM process_instances WHERE id = ?");
            $stmt_instance->execute([$instanceId]);

            $this->pdo->commit();
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }

    public function getOrCreateMeeting(int $bniGroupId, string $meetingDatetime): int {
        $meetingKey = $bniGroupId . '_' . $meetingDatetime;
        $stmt = $this->pdo->prepare("SELECT id FROM meetings WHERE meeting_key = ?");
        $stmt->execute([$meetingKey]);
        $meetingId = $stmt->fetchColumn();

        if (!$meetingId) {
            $stmt = $this->pdo->prepare("INSERT INTO meetings (bni_group_id, meeting_date, meeting_datetime, meeting_key) VALUES (?, DATE(?), ?, ?)");
            $stmt->execute([$bniGroupId, $meetingDatetime, $meetingDatetime, $meetingKey]);
            $meetingId = $this->pdo->lastInsertId();
        }

        return (int)$meetingId;
    }

    public function getMeetingAttendance(int $meetingId): array {
        $stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ?");
        $stmt->execute([$meetingId]);
        $attendance_raw = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $attendance = [];
        foreach ($attendance_raw as $att) {
            $attendance[$att['person_id']] = $att;
        }

        return $attendance;
    }

    public function updateMeetingAttendance(int $meetingId, int $personId, int $bniGroupId, string $status, int $userId, ?string $guestSurvey = null): void {
        $sql = "INSERT INTO meeting_attendance (meeting_id, person_id, bni_group_id, attendance_status, guest_survey, updated_by) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE attendance_status = VALUES(attendance_status), guest_survey = VALUES(guest_survey), updated_by = VALUES(updated_by), bni_group_id = VALUES(bni_group_id)";
        
        // Log query and params
        $params = [$meetingId, $personId, $bniGroupId, $status, $guestSurvey, $userId];
        error_log("SQL: $sql");
        error_log("Params: " . json_encode($params));

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
    }

    public function getMeetingAttendanceByGroupAndDate(int $groupId, string $meetingDatetime): array {
        $meetingId = $this->getOrCreateMeeting($groupId, $meetingDatetime);
        return $this->getMeetingAttendance($meetingId);
    }

    public function isMemberOfGroup(int $personId, int $bniGroupId): bool {
        $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM people WHERE id = ? AND bni_group_id = ?");
        $stmt->execute([$personId, $bniGroupId]);
        return (int)$stmt->fetchColumn() > 0;
    }

    public function getMeetingDetails(int $personId, int $bniGroupId, string $meetingDatetime): array {
        $meetingId = $this->getOrCreateMeeting($bniGroupId, $meetingDatetime);

        $stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ? AND person_id = ?");
        $stmt->execute([$meetingId, $personId]);
        $attendance = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($attendance) {
            return $attendance;
        }

        // If no record, return default state
        $isMember = $this->isMemberOfGroup($personId, $bniGroupId);
        return [
            'meeting_id' => $meetingId,
            'person_id' => $personId,
            'attendance_status' => $isMember ? 'present' : 'none',
            'guest_survey' => null,
        ];
    }

    public function getPeopleDetails(array $personIds): array {
        if (empty($personIds)) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($personIds), '?'));

        $sql = "SELECT p.id, p.first_name, p.last_name, p.company_name, p.industry, bg.name as bni_group_name 
                FROM people p 
                LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id
                WHERE p.id IN ($placeholders)";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($personIds);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}