<?php
declare(strict_types=1);

class TaskController
{
    public static function create(int $projectId): void
    {
        Auth::requireLogin();
        if (!Auth::canCreateTask($projectId)) {
            Helper::redirect('/project/' . $projectId, ['error' => 'access_denied']);
        }

        $db      = Database::getInstance();
        $project = $db->fetch('SELECT * FROM projects WHERE id = ?', [$projectId]);
        if (!$project) Helper::redirect('/projects');

        $requiredFields = json_decode($project['required_fields'] ?? '[]', true) ?: [];
        $statuses       = $db->fetchAll('SELECT * FROM task_statuses WHERE project_id = ? ORDER BY position', [$projectId]);
        $members        = $db->fetchAll(
            'SELECT pm.user_id, pm.user_id as id, u.username, u.avatar FROM project_members pm JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? ORDER BY u.username',
            [$projectId]
        );
        $labels  = $db->fetchAll('SELECT * FROM labels WHERE project_id = ?', [$projectId]);
        $projectExternals = $db->fetchAll('SELECT * FROM project_externals WHERE project_id = ? ORDER BY name', [$projectId]);
        $errors  = [];
        $allProjectTasks = $db->fetchAll(
            'SELECT id, title FROM tasks WHERE project_id = ? ORDER BY title',
            [$projectId]
        );
        $predecessorIds = [];
        $successorIds   = [];
        $assignedUserIds     = [];
        $assignedExternalIds = [];
        $assignedExternals   = [];

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $predecessorIds = array_filter(array_map('intval', Helper::post('predecessors', [])));
            $successorIds   = array_filter(array_map('intval', Helper::post('successors', [])));
            $assignedUserIds     = array_filter(array_map('intval', Helper::post('assignee_user_ids', [])));
            $assignedExternalIds = array_filter(array_map('intval', Helper::post('assignee_external_ids', [])));
            $assignedExternals   = array_filter(array_map('trim', Helper::post('assignee_external_names', [])));
            $data = self::extractTaskData($requiredFields, $errors);

            if (empty($errors)) {
                $db->beginTransaction();
                try {
                    $taskId = $db->insert(
                        'INSERT INTO tasks (project_id, title, description, status_id, priority,
                                           created_by, due_date, start_date, position)
                         VALUES (?,?,?,?,?,?,?,?,
                             (SELECT COALESCE(MAX(position),0)+1 FROM tasks WHERE project_id = ? AND status_id = ?))',
                        [
                            $projectId, $data['title'], $data['description'],
                            $data['status_id'], $data['priority'],
                            Auth::id(), $data['due_date'], $data['start_date'],
                            $projectId, $data['status_id']
                        ]
                    );

                    // Labels
                    foreach ($data['labels'] as $labelId) {
                        $db->execute('INSERT INTO task_labels (task_id, label_id) VALUES (?,?)', [$taskId, $labelId]);
                    }

                    // Predecessors (other tasks → this task)
                    foreach ($predecessorIds as $predId) {
                        if ($predId > 0 && $predId !== (int)$taskId) {
                            $db->execute(
                                'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                                [$predId, $taskId]
                            );
                        }
                    }

                    // Successors (this task → other tasks)
                    foreach ($successorIds as $succId) {
                        if ($succId > 0 && $succId !== (int)$taskId) {
                            $db->execute(
                                'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                                [$taskId, $succId]
                            );
                        }
                    }

                    // Assignees
                    foreach ($assignedUserIds as $uid) {
                        if ($uid > 0) {
                            $db->execute('INSERT INTO task_assignees (task_id, user_id) VALUES (?,?)', [$taskId, $uid]);
                        }
                    }
                    foreach ($assignedExternalIds as $extId) {
                        if ($extId > 0) {
                            $db->execute('INSERT INTO task_assignees (task_id, external_id) VALUES (?,?)', [$taskId, $extId]);
                        }
                    }
                    foreach ($assignedExternals as $extName) {
                        if ($extName !== '') {
                            $db->execute('INSERT INTO task_assignees (task_id, external_name) VALUES (?,?)', [$taskId, $extName]);
                        }
                    }

                    // Attachment
                    if (!empty($_FILES['attachment']['name'])) {
                        $uploaded = Helper::uploadFile($_FILES['attachment'], (int)$taskId);
                        if ($uploaded) {
                            $db->execute(
                                'INSERT INTO attachments (task_id, user_id, filename, original_name, file_size, mime_type) VALUES (?,?,?,?,?,?)',
                                [$taskId, Auth::id(), $uploaded['filename'], $uploaded['original_name'], $uploaded['file_size'], $uploaded['mime_type']]
                            );
                        }
                    }

                    $db->commit();

                    Helper::logActivity(Auth::id(), 'task_created', $data['title'], $projectId, (int)$taskId);

                    $task    = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$taskId]);
                    $creator = Auth::user();
                    Mailer::notifyManagerActivity($task, $project, Lang::get('activity_task_created'), $creator);
                    Mailer::notifyManagerTaskCreated($task, $project, $creator);

                    // Notify each newly assigned user
                    foreach ($assignedUserIds as $uid) {
                        if ($uid > 0 && $uid !== Auth::id()) {
                            $assignee = $db->fetch('SELECT id, username, email FROM users WHERE id = ? AND is_active = 1', [$uid]);
                            if ($assignee) Mailer::notifyAssignment($task, $project, $assignee);
                        }
                    }
                    foreach ($assignedExternalIds as $extId) {
                        $ext = $db->fetch('SELECT * FROM project_externals WHERE id = ?', [$extId]);
                        if ($ext) Mailer::notifyExternalAssignment($task, $project, $ext);
                    }

                    Helper::redirect('/task/' . $taskId, ['success' => Lang::get('task_created')]);
                } catch (\Throwable $e) {
                    $db->rollback();
                    error_log('[Plenvo] Task save error: ' . $e->getMessage());
                    $errors[] = t('error');
                }
            }
        }

        view('tasks/form', compact('project', 'statuses', 'members', 'labels', 'projectExternals', 'requiredFields', 'errors', 'parentId', 'allProjectTasks', 'predecessorIds', 'successorIds', 'assignedUserIds', 'assignedExternalIds', 'assignedExternals'));
    }

    public static function show(int $id): void
    {
        Auth::requireLogin();
        $db   = Database::getInstance();
        $task = $db->fetch(
            'SELECT t.*, p.name as project_name, p.color as project_color,
                    p.required_fields, p.id as project_id,
                    ts.name as status_name, ts.color as status_color,
                    u.username as assignee_name,
                    c.username as creator_name
             FROM tasks t
             JOIN projects p ON p.id = t.project_id
             LEFT JOIN task_statuses ts ON ts.id = t.status_id
             LEFT JOIN users u ON u.id = t.assigned_to
             LEFT JOIN users c ON c.id = t.created_by
             WHERE t.id = ?',
            [$id]
        );
        if (!$task) Helper::redirect('/dashboard');
        Auth::requireProjectAccess($task['project_id']);

        $comments = $db->fetchAll(
            'SELECT c.*, u.username, u.avatar FROM comments c LEFT JOIN users u ON u.id = c.user_id
             WHERE c.task_id = ? ORDER BY c.created_at',
            [$id]
        );

        $attachments = $db->fetchAll(
            'SELECT a.*, u.username FROM attachments a JOIN users u ON u.id = a.user_id
             WHERE a.task_id = ? ORDER BY a.created_at DESC',
            [$id]
        );

        $taskLabels = $db->fetchAll(
            'SELECT l.* FROM task_labels tl JOIN labels l ON l.id = tl.label_id WHERE tl.task_id = ?',
            [$id]
        );

        $statuses = $db->fetchAll('SELECT * FROM task_statuses WHERE project_id = ? ORDER BY position', [$task['project_id']]);
        $members  = $db->fetchAll(
            'SELECT pm.user_id, pm.user_id as id, u.username, u.avatar FROM project_members pm JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? ORDER BY u.username',
            [$task['project_id']]
        );
        $labels      = $db->fetchAll('SELECT * FROM labels WHERE project_id = ?', [$task['project_id']]);
        $myRole  = Auth::getProjectRole($task['project_id']);
        $success = Helper::flash('success');
        $error       = Helper::flash('error');

        $predecessors = $db->fetchAll(
            'SELECT t.id, t.title, ts.name as status_name, ts.color as status_color
             FROM task_dependencies td
             JOIN tasks t ON t.id = td.predecessor_id
             LEFT JOIN task_statuses ts ON ts.id = t.status_id
             WHERE td.successor_id = ?
             ORDER BY t.title',
            [$id]
        );
        $successors = $db->fetchAll(
            'SELECT t.id, t.title, ts.name as status_name, ts.color as status_color
             FROM task_dependencies td
             JOIN tasks t ON t.id = td.successor_id
             LEFT JOIN task_statuses ts ON ts.id = t.status_id
             WHERE td.predecessor_id = ?
             ORDER BY t.title',
            [$id]
        );

        $allProjectTasks = $db->fetchAll(
            'SELECT id, title FROM tasks WHERE project_id = ? AND id != ? ORDER BY title',
            [$task['project_id'], $id]
        );

        $assignees = $db->fetchAll(
            'SELECT ta.*, u.username, u.avatar,
                    COALESCE(pe.name, ta.external_name) AS external_name,
                    pe.token AS external_token
             FROM task_assignees ta
             LEFT JOIN users u ON u.id = ta.user_id
             LEFT JOIN project_externals pe ON pe.id = ta.external_id
             WHERE ta.task_id = ? ORDER BY ta.id',
            [$id]
        );
        $projectExternals = $db->fetchAll('SELECT * FROM project_externals WHERE project_id = ? ORDER BY name', [$task['project_id']]);

        view('tasks/detail', compact(
            'task', 'comments', 'attachments',
            'taskLabels', 'statuses', 'members', 'labels', 'myRole',
            'success', 'error', 'predecessors', 'successors',
            'allProjectTasks', 'assignees', 'projectExternals'
        ));
    }

    public static function edit(int $id): void
    {
        Auth::requireLogin();
        $db   = Database::getInstance();
        $task = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$id]);
        if (!$task) Helper::redirect('/dashboard');

        Auth::requireProjectAccess($task['project_id']);
        if (!Auth::canEditTask($task['project_id'])) {
            Helper::redirect('/task/' . $id, ['error' => 'access_denied']);
        }

        $project        = $db->fetch('SELECT * FROM projects WHERE id = ?', [$task['project_id']]);
        $requiredFields = json_decode($project['required_fields'] ?? '[]', true) ?: [];
        $statuses       = $db->fetchAll('SELECT * FROM task_statuses WHERE project_id = ? ORDER BY position', [$task['project_id']]);
        $members        = $db->fetchAll(
            'SELECT pm.user_id, pm.user_id as id, u.username, u.avatar FROM project_members pm JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? ORDER BY u.username',
            [$task['project_id']]
        );
        $labels     = $db->fetchAll('SELECT * FROM labels WHERE project_id = ?', [$task['project_id']]);
        $projectExternals = $db->fetchAll('SELECT * FROM project_externals WHERE project_id = ? ORDER BY name', [$task['project_id']]);
        $taskLabels = array_column($db->fetchAll('SELECT label_id FROM task_labels WHERE task_id = ?', [$id]), 'label_id');
        $errors     = [];
        $allProjectTasks = $db->fetchAll(
            'SELECT id, title FROM tasks WHERE project_id = ? AND id != ? ORDER BY title',
            [$task['project_id'], $id]
        );
        $predecessorIds = array_column(
            $db->fetchAll('SELECT predecessor_id FROM task_dependencies WHERE successor_id = ?', [$id]),
            'predecessor_id'
        );
        $successorIds = array_column(
            $db->fetchAll('SELECT successor_id FROM task_dependencies WHERE predecessor_id = ?', [$id]),
            'successor_id'
        );
        $assignedUserIds = array_column(
            $db->fetchAll('SELECT user_id FROM task_assignees WHERE task_id = ? AND user_id IS NOT NULL', [$id]),
            'user_id'
        );
        $assignedExternalIds = array_column(
            $db->fetchAll('SELECT external_id FROM task_assignees WHERE task_id = ? AND external_id IS NOT NULL', [$id]),
            'external_id'
        );
        $assignedExternals = array_column(
            $db->fetchAll('SELECT external_name FROM task_assignees WHERE task_id = ? AND external_name IS NOT NULL AND external_id IS NULL', [$id]),
            'external_name'
        );

        if (Helper::isPost()) {
            Helper::verifyCsrf();
            $action = Helper::post('action', 'update');
            // Re-read from POST so the form re-populates on validation errors
            if (in_array($action, ['update', 'upload_only', 'change_status'])) {
                $postedPredecessors = array_filter(array_map('intval', Helper::post('predecessors', [])));
                if (!empty($postedPredecessors)) $predecessorIds = $postedPredecessors;
                $postedSuccessors = array_filter(array_map('intval', Helper::post('successors', [])));
                if (!empty($postedSuccessors)) $successorIds = $postedSuccessors;
                $newUserIds = array_filter(array_map('intval', Helper::post('assignee_user_ids', [])));
                if (!empty($newUserIds)) $assignedUserIds = $newUserIds;
                $newExtIds = array_filter(array_map('intval', Helper::post('assignee_external_ids', [])));
                if (!empty($newExtIds)) $assignedExternalIds = $newExtIds;
                $newExtNames = array_filter(array_map('trim', Helper::post('assignee_external_names', [])));
                if (!empty($newExtNames)) $assignedExternals = $newExtNames;
            }

            if ($action === 'delete') {
                $db->execute('DELETE FROM tasks WHERE id = ?', [$id]);
                Helper::logActivity(Auth::id(), 'task_deleted', $task['title'], $task['project_id']);
                Helper::redirect('/project/' . $task['project_id'], ['success' => Lang::get('task_deleted')]);
            }

            if ($action === 'update_dates') {
                $startDate = Helper::post('start_date') ?: null;
                $dueDate   = Helper::post('due_date')   ?: null;
                $db->execute(
                    'UPDATE tasks SET start_date=?, due_date=?, updated_at=CURRENT_TIMESTAMP WHERE id=?',
                    [$startDate, $dueDate, $id]
                );
                Helper::logActivity(Auth::id(), 'dates_updated', $task['title'], $task['project_id'], $id);
                if ($task['due_date'] !== $dueDate) {
                    Mailer::notifyAssigneesOnChange($db->fetch('SELECT * FROM tasks WHERE id=?', [$id]), $project, Auth::user());
                }
                Helper::redirect('/task/' . $id);
            }

            if ($action === 'update_assignees') {
                $oldUids   = array_column($db->fetchAll('SELECT user_id FROM task_assignees WHERE task_id = ? AND user_id IS NOT NULL', [$id]), 'user_id');
                $oldExtIds = array_column($db->fetchAll('SELECT external_id FROM task_assignees WHERE task_id = ? AND external_id IS NOT NULL', [$id]), 'external_id');
                $db->execute('DELETE FROM task_assignees WHERE task_id = ?', [$id]);
                // Only allow users/externals that are actual project members
                $validUids    = array_column($db->fetchAll('SELECT user_id FROM project_members WHERE project_id = ?', [$task['project_id']]), 'user_id');
                $validExtIds  = array_column($db->fetchAll('SELECT id FROM project_externals WHERE project_id = ?', [$task['project_id']]), 'id');
                $newUids   = array_values(array_filter(array_map('intval', Helper::post('assignee_user_ids', [])), fn($uid) => $uid > 0 && in_array($uid, $validUids)));
                $newExtIds = array_values(array_filter(array_map('intval', Helper::post('assignee_external_ids', [])), fn($eid) => $eid > 0 && in_array($eid, $validExtIds)));
                foreach ($newUids as $uid) {
                    $db->execute('INSERT INTO task_assignees (task_id, user_id) VALUES (?,?)', [$id, $uid]);
                }
                foreach ($newExtIds as $extId) {
                    $db->execute('INSERT INTO task_assignees (task_id, external_id) VALUES (?,?)', [$id, $extId]);
                }
                foreach (array_filter(array_map('trim', Helper::post('assignee_external_names', []))) as $extName) {
                    if ($extName !== '') $db->execute('INSERT INTO task_assignees (task_id, external_name) VALUES (?,?)', [$id, $extName]);
                }
                Helper::logActivity(Auth::id(), 'assignees_updated', $task['title'], $task['project_id'], $id);
                $actor = Auth::user();
                $addedUids   = array_values(array_diff($newUids,   $oldUids));
                $addedExtIds = array_values(array_diff($newExtIds, $oldExtIds));
                $removedUids = array_values(array_diff($oldUids,   $newUids));
                $removedExts = array_values(array_diff($oldExtIds, $newExtIds));
                // Assignee-only edit: no general "task changed" mail to the
                // unchanged assignees. They are still on the task; nothing
                // about its content changed for them.
                if (!empty($addedUids) || !empty($addedExtIds) || !empty($removedUids) || !empty($removedExts)) {
                    Mailer::notifyManagerAssigneesChanged($task, $project, $actor);
                }
                foreach ($addedUids as $uid) {
                    if ($uid > 0 && $uid !== Auth::id()) {
                        $assignee = $db->fetch('SELECT id, username, email FROM users WHERE id = ? AND is_active = 1', [$uid]);
                        if ($assignee) Mailer::notifyAssignment($task, $project, $assignee);
                    }
                }
                foreach ($addedExtIds as $extId) {
                    $ext = $db->fetch('SELECT * FROM project_externals WHERE id = ?', [$extId]);
                    if ($ext) Mailer::notifyExternalAssignment($task, $project, $ext);
                }
                Mailer::notifyAssigneesOnUnassign($task, $project, $removedUids, $removedExts);
                Helper::redirect('/task/' . $id);
            }

            if ($action === 'upload_only') {
                if (!empty($_FILES['attachment']['name'])) {
                    $uploaded = Helper::uploadFile($_FILES['attachment'], $id);
                    if ($uploaded) {
                        $db->execute(
                            'INSERT INTO attachments (task_id, user_id, filename, original_name, file_size, mime_type) VALUES (?,?,?,?,?,?)',
                            [$id, Auth::id(), $uploaded['filename'], $uploaded['original_name'], $uploaded['file_size'], $uploaded['mime_type']]
                        );
                        Helper::logActivity(Auth::id(), 'attachment_added', $task['title'], $task['project_id'], $id);
                        Mailer::notifyAssigneesOnAttachment($task, $project, Auth::user());
                    }
                }
                Helper::redirect('/task/' . $id, ['success' => Lang::get('file_uploaded')]);
            }

            if ($action === 'change_status') {
                $newStatusId = (int)Helper::post('status_id');
                if ($newStatusId) {
                    $db->execute('UPDATE tasks SET status_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?', [$newStatusId, $id]);
                    Helper::logActivity(Auth::id(), 'status_changed', $task['title'], $task['project_id'], $id);
                    $actor     = Auth::user();
                    $newStatus = $db->fetch('SELECT * FROM task_statuses WHERE id = ?', [$newStatusId]);
                    Mailer::notifyManagerActivity($task, $project, Lang::get('activity_status_changed'), $actor);
                    if (!empty($newStatus['is_done'])) {
                        Mailer::notifyManagerTaskDone($task, $project, $actor);
                        self::checkAndNotifySuccessors((int)$id, $project, $db);
                    }
                }
                Helper::redirect('/task/' . $id, ['success' => Lang::get('task_updated')]);
            }

            if ($action === 'change_priority') {
                $priority = Helper::post('priority', 'medium');
                if (in_array($priority, ['low','medium','high','urgent'])) {
                    $db->execute('UPDATE tasks SET priority=?, updated_at=CURRENT_TIMESTAMP WHERE id=?', [$priority, $id]);
                    Helper::logActivity(Auth::id(), 'activity_task_updated', $task['title'], $task['project_id'], $id);
                    $actor = Auth::user();
                    Mailer::notifyManagerActivity($task, $project, Lang::get('activity_task_updated'), $actor);
                    if ($task['priority'] !== $priority) {
                        Mailer::notifyAssigneesOnChange($db->fetch('SELECT * FROM tasks WHERE id=?', [$id]), $project, $actor);
                    }
                }
                Helper::redirect('/task/' . $id);
            }

            if ($action === 'update_dependencies') {
                // Only allow tasks within the same project
                $validTaskIds = array_column($db->fetchAll('SELECT id FROM tasks WHERE project_id = ?', [$task['project_id']]), 'id');

                // Predecessors: tasks that must come before this one (this task is the successor)
                $newPredIds = array_filter(array_map('intval', Helper::post('predecessors', [])));
                $db->execute('DELETE FROM task_dependencies WHERE successor_id = ?', [$id]);
                foreach ($newPredIds as $predId) {
                    if ($predId > 0 && $predId !== $id && in_array($predId, $validTaskIds)) {
                        $db->execute(
                            'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                            [$predId, $id]
                        );
                    }
                }

                // Successors: tasks that come after this one (this task is the predecessor)
                $newSuccIds = array_filter(array_map('intval', Helper::post('successors', [])));
                $db->execute('DELETE FROM task_dependencies WHERE predecessor_id = ?', [$id]);
                foreach ($newSuccIds as $succId) {
                    if ($succId > 0 && $succId !== $id && in_array($succId, $validTaskIds)) {
                        $db->execute(
                            'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                            [$id, $succId]
                        );
                    }
                }

                Helper::logActivity(Auth::id(), 'dependencies_updated', $task['title'], $task['project_id'], $id);
                Helper::redirect('/task/' . $id);
            }

            $data = self::extractTaskData($requiredFields, $errors);

            if (empty($errors)) {
                $oldStatus = $task['status_id'];
                $db->execute(
                    'UPDATE tasks SET title=?, description=?, status_id=?, priority=?,
                                     due_date=?, start_date=?, updated_at=CURRENT_TIMESTAMP
                     WHERE id=?',
                    [
                        $data['title'], $data['description'], $data['status_id'], $data['priority'],
                        $data['due_date'], $data['start_date'],
                        $id
                    ]
                );

                // Sync labels
                $db->execute('DELETE FROM task_labels WHERE task_id = ?', [$id]);
                foreach ($data['labels'] as $labelId) {
                    $db->execute('INSERT INTO task_labels (task_id, label_id) VALUES (?,?)', [$id, $labelId]);
                }

                // Validate tasks/users/externals belong to this project
                $validTaskIds2 = array_column($db->fetchAll('SELECT id FROM tasks WHERE project_id = ?', [$task['project_id']]), 'id');
                $validUids2    = array_column($db->fetchAll('SELECT user_id FROM project_members WHERE project_id = ?', [$task['project_id']]), 'user_id');
                $validExtIds2  = array_column($db->fetchAll('SELECT id FROM project_externals WHERE project_id = ?', [$task['project_id']]), 'id');

                // Sync predecessors (other tasks → this task)
                $newPredIds = array_filter(array_map('intval', Helper::post('predecessors', [])));
                $db->execute('DELETE FROM task_dependencies WHERE successor_id = ?', [$id]);
                foreach ($newPredIds as $predId) {
                    if ($predId > 0 && $predId !== $id && in_array($predId, $validTaskIds2)) {
                        $db->execute(
                            'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                            [$predId, $id]
                        );
                    }
                }
                $predecessorIds = array_values($newPredIds);

                // Sync successors (this task → other tasks)
                $newSuccIds = array_filter(array_map('intval', Helper::post('successors', [])));
                $db->execute('DELETE FROM task_dependencies WHERE predecessor_id = ?', [$id]);
                foreach ($newSuccIds as $succId) {
                    if ($succId > 0 && $succId !== $id && in_array($succId, $validTaskIds2)) {
                        $db->execute(
                            'INSERT OR IGNORE INTO task_dependencies (predecessor_id, successor_id) VALUES (?,?)',
                            [$id, $succId]
                        );
                    }
                }
                $successorIds = array_values($newSuccIds);

                // Sync assignees
                $oldUids2   = array_column($db->fetchAll('SELECT user_id FROM task_assignees WHERE task_id = ? AND user_id IS NOT NULL', [$id]), 'user_id');
                $oldExtIds2 = array_column($db->fetchAll('SELECT external_id FROM task_assignees WHERE task_id = ? AND external_id IS NOT NULL', [$id]), 'external_id');
                $db->execute('DELETE FROM task_assignees WHERE task_id = ?', [$id]);
                $newUserIds = array_values(array_filter(array_map('intval', Helper::post('assignee_user_ids', [])), fn($uid) => $uid > 0 && in_array($uid, $validUids2)));
                $newExtIds  = array_values(array_filter(array_map('intval', Helper::post('assignee_external_ids', [])), fn($eid) => $eid > 0 && in_array($eid, $validExtIds2)));
                foreach ($newUserIds as $uid) {
                    $db->execute('INSERT INTO task_assignees (task_id, user_id) VALUES (?,?)', [$id, $uid]);
                }
                foreach ($newExtIds as $extId) {
                    $db->execute('INSERT INTO task_assignees (task_id, external_id) VALUES (?,?)', [$id, $extId]);
                }
                $newDummyNames = array_filter(array_map('trim', Helper::post('assignee_external_names', [])));
                foreach ($newDummyNames as $extName) {
                    if ($extName !== '') $db->execute('INSERT INTO task_assignees (task_id, external_name) VALUES (?,?)', [$id, $extName]);
                }
                $assignedUserIds     = array_values($newUserIds);
                $assignedExternalIds = array_values($newExtIds);
                $assignedExternals   = array_values($newDummyNames);

                // New attachment
                if (!empty($_FILES['attachment']['name'])) {
                    $uploaded = Helper::uploadFile($_FILES['attachment'], $id);
                    if ($uploaded) {
                        $db->execute(
                            'INSERT INTO attachments (task_id, user_id, filename, original_name, file_size, mime_type) VALUES (?,?,?,?,?,?)',
                            [$id, Auth::id(), $uploaded['filename'], $uploaded['original_name'], $uploaded['file_size'], $uploaded['mime_type']]
                        );
                    }
                }

                $activity = 'task_updated';
                if ($oldStatus != $data['status_id']) {
                    $newSt = $db->fetch('SELECT name FROM task_statuses WHERE id = ?', [$data['status_id']]);
                    $activity = 'status_changed → ' . ($newSt['name'] ?? '');
                }

                Helper::logActivity(Auth::id(), $activity, $data['title'], $task['project_id'], $id);

                $updatedTask = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$id]);
                $user        = Auth::user();
                Mailer::notifyManagerActivity($updatedTask, $project, $activity, $user);

                $addedUids2   = array_values(array_diff($assignedUserIds,     $oldUids2));
                $addedExtIds2 = array_values(array_diff($assignedExternalIds, $oldExtIds2));
                $removedUids2 = array_values(array_diff($oldUids2,            $assignedUserIds));
                $removedExts2 = array_values(array_diff($oldExtIds2,          $assignedExternalIds));

                foreach ($addedUids2 as $uid) {
                    if ($uid > 0 && $uid !== Auth::id()) {
                        $assignee = $db->fetch('SELECT id, username, email FROM users WHERE id = ? AND is_active = 1', [$uid]);
                        if ($assignee) Mailer::notifyAssignment($updatedTask, $project, $assignee);
                    }
                }
                foreach ($addedExtIds2 as $extId) {
                    $ext = $db->fetch('SELECT * FROM project_externals WHERE id = ?', [$extId]);
                    if ($ext) Mailer::notifyExternalAssignment($updatedTask, $project, $ext);
                }
                Mailer::notifyAssigneesOnUnassign($updatedTask, $project, $removedUids2, $removedExts2);

                // Manager pings only if the assignee set actually changed and
                // the manager has opted into "assignee churn" pings explicitly.
                if (!empty($addedUids2) || !empty($addedExtIds2) || !empty($removedUids2) || !empty($removedExts2)) {
                    Mailer::notifyManagerAssigneesChanged($updatedTask, $project, $user);
                }

                // Notify assignees if relevant content fields changed. Newly-added
                // assignees are excluded — they already received an "assignment"
                // mail in the loop above and would otherwise get a duplicate
                // "task changed" mail in the same request.
                if ($task['due_date'] !== $data['due_date']
                    || $task['priority'] !== $data['priority']
                    || $task['description'] !== $data['description']) {
                    Mailer::notifyAssigneesOnChange($updatedTask, $project, $user, $addedUids2, $addedExtIds2);
                }

                // If status changed to done
                if ($oldStatus != $data['status_id']) {
                    $newStatus = $db->fetch('SELECT * FROM task_statuses WHERE id = ?', [$data['status_id']]);
                    if (!empty($newStatus['is_done'])) {
                        Mailer::notifyManagerTaskDone($updatedTask, $project, $user);
                        self::checkAndNotifySuccessors((int)$id, $project, $db);
                    }
                }

                Helper::redirect('/task/' . $id, ['success' => Lang::get('task_updated')]);
            }
        }

        view('tasks/form', compact('project', 'task', 'statuses', 'members', 'labels', 'projectExternals', 'taskLabels', 'requiredFields', 'errors', 'allProjectTasks', 'predecessorIds', 'successorIds', 'assignedUserIds', 'assignedExternalIds', 'assignedExternals'));
    }

    public static function comment(int $taskId): void
    {
        Auth::requireLogin();
        $db   = Database::getInstance();
        $task = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$taskId]);
        if (!$task) Helper::json(['error' => 'Not found'], 404);

        Auth::requireProjectAccess($task['project_id']);
        Helper::verifyCsrf();

        $content = Helper::sanitizeHtml(trim(Helper::post('content')));
        if (empty(trim(strip_tags($content)))) {
            Helper::redirect('/task/' . $taskId);
        }

        $db->execute(
            'INSERT INTO comments (task_id, user_id, content) VALUES (?,?,?)',
            [$taskId, Auth::id(), $content]
        );
        Helper::logActivity(Auth::id(), 'comment', $task['title'], $task['project_id'], $taskId);

        $project = $db->fetch('SELECT * FROM projects WHERE id = ?', [$task['project_id']]);
        $user    = Auth::user();
        Mailer::notifyManagerActivity($task, $project, Lang::get('activity_comment'), $user);
        Mailer::notifyAssigneesOnComment($task, $project, $user);

        preg_match_all('/@\[([^\]]+)\]/', $content, $m1);
        preg_match_all('/data-mention="([^"]+)"/', $content, $m2);
        $mentionedNames = array_merge($m1[1] ?? [], $m2[1] ?? []);
        if (!empty($mentionedNames)) {
            Mailer::notifyMentioned($task, $project, $mentionedNames, $user);
        }

        Helper::redirect('/task/' . $taskId, ['success' => Lang::get('comment_posted')]);
    }

    public static function deleteAttachment(int $attachmentId): void
    {
        Auth::requireLogin();
        Helper::verifyCsrf();

        $db = Database::getInstance();
        $att = $db->fetch('SELECT * FROM attachments WHERE id = ?', [$attachmentId]);
        if (!$att) Helper::redirect('/dashboard');

        $task = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$att['task_id']]);
        Auth::requireProjectAccess($task['project_id']);

        // Only creator or project manager can delete
        if ($att['user_id'] !== Auth::id() && !Auth::canEditProject($task['project_id'])) {
            Helper::redirect('/task/' . $att['task_id'], ['error' => 'access_denied']);
        }

        $path     = realpath(UPLOAD_PATH . '/' . $att['filename']);
        $basePath = realpath(UPLOAD_PATH);
        if ($path && $basePath && str_starts_with($path, $basePath . DIRECTORY_SEPARATOR)) {
            @unlink($path);
        }

        $db->execute('DELETE FROM attachments WHERE id = ?', [$attachmentId]);
        Helper::redirect('/task/' . $att['task_id'], ['success' => 'Anhang gelöscht']);
    }

    public static function download(int $attachmentId): void
    {
        Auth::requireLogin();
        $db  = Database::getInstance();
        $att = $db->fetch('SELECT * FROM attachments WHERE id = ?', [$attachmentId]);
        if (!$att) http_response_code(404) and exit;

        $task = $db->fetch('SELECT * FROM tasks WHERE id = ?', [$att['task_id']]);
        Auth::requireProjectAccess($task['project_id']);

        $basePath = realpath(UPLOAD_PATH);
        $path     = realpath(UPLOAD_PATH . '/' . $att['filename']);
        if (!$path || !$basePath || !str_starts_with($path, $basePath . DIRECTORY_SEPARATOR)) {
            http_response_code(403); exit;
        }
        if (!file_exists($path)) { http_response_code(404); exit; }

        $mime = $att['mime_type'] ?: 'application/octet-stream';
        header('Content-Type: ' . $mime);
        header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
        header('Content-Length: ' . filesize($path));
        header('Cache-Control: private, max-age=0');
        readfile($path);
        exit;
    }

    private static function extractTaskData(array $requiredFields, array &$errors): array
    {
        $title          = trim(Helper::post('title'));
        $description    = Helper::sanitizeHtml(trim(Helper::post('description')));
        $statusId       = (int)Helper::post('status_id') ?: null;
        $priority       = Helper::post('priority', 'medium');
        $dueDate        = Helper::post('due_date') ?: null;
        $startDate      = Helper::post('start_date') ?: null;
        $selectedLabels = Helper::post('labels', []);

        if (empty($title)) $errors[] = t('title_required');
        if (in_array('description', $requiredFields) && empty($description)) $errors[] = t('description_required');
        if (in_array('due_date', $requiredFields) && empty($dueDate)) $errors[] = t('due_date_required');

        if (!in_array($priority, ['low', 'medium', 'high', 'urgent'])) $priority = 'medium';

        return compact('title', 'description', 'statusId', 'priority',
                       'dueDate', 'startDate', 'selectedLabels') +
               ['status_id' => $statusId,
                'due_date' => $dueDate, 'start_date' => $startDate,
                'labels' => $selectedLabels];
    }

    /**
     * After a task is marked done, check each successor:
     * if ALL of its predecessors are now done, notify its assignees.
     */
    public static function checkAndNotifySuccessors(int $doneTaskId, array $project, $db): void
    {
        $successors = $db->fetchAll(
            'SELECT successor_id FROM task_dependencies WHERE predecessor_id = ?',
            [$doneTaskId]
        );
        foreach ($successors as $row) {
            $succId = (int)$row['successor_id'];
            // Count total predecessors vs done predecessors
            $total = (int)$db->fetch(
                'SELECT COUNT(*) as c FROM task_dependencies WHERE successor_id = ?',
                [$succId]
            )['c'];
            $done = (int)$db->fetch(
                'SELECT COUNT(*) as c FROM task_dependencies td
                 JOIN tasks t ON t.id = td.predecessor_id
                 JOIN task_statuses ts ON ts.id = t.status_id
                 WHERE td.successor_id = ? AND ts.is_done = 1',
                [$succId]
            )['c'];
            if ($total > 0 && $total === $done) {
                Mailer::notifyPredecessorsDone($succId, $project);
            }
        }
    }
}
