<?php
declare(strict_types=1);

class ExternalController
{
    private const MFA_TIMEOUT = 3 * 3600; // 3 hours inactivity

    private static function findExternal(string $token): array|false
    {
        if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false;
        $db = Database::getInstance();
        return $db->fetch(
            'SELECT pe.*, p.name AS project_name, p.color AS project_color, p.id AS project_id
             FROM project_externals pe
             JOIN projects p ON p.id = pe.project_id
             WHERE pe.token = ?',
            [$token]
        );
    }

    // Returns true if the external user has a valid MFA session for this token.
    private static function isMfaAuthorized(string $token): bool
    {
        $last = $_SESSION['ext_auth'][$token] ?? 0;
        return $last > 0 && (time() - $last) < self::MFA_TIMEOUT;
    }

    // Refreshes the activity timestamp so the 3h window resets on each interaction.
    private static function touchMfaSession(string $token): void
    {
        $_SESSION['ext_auth'][$token] = time();
    }

    // Sends a new 6-digit code to the external user and stores the hash in DB.
    private static function sendNewCode(Database $db, array $external): void
    {
        $db->execute(
            'UPDATE external_mfa_codes SET used_at = ? WHERE external_id = ? AND used_at IS NULL',
            [time(), $external['id']]
        );
        $code = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        $db->execute(
            'INSERT INTO external_mfa_codes (external_id, code_hash, expires_at) VALUES (?, ?, ?)',
            [$external['id'], password_hash($code, PASSWORD_BCRYPT, ['cost' => Auth::BCRYPT_COST]), time() + 1200]
        );
        Mailer::sendExternalMfaCode($external['email'], $external['name'], $code);
    }

    // Gate: redirects to verify page if MFA is required and not yet passed.
    private static function requireMfa(string $token, array $external): void
    {
        if (empty($external['email'])) return; // no email = no MFA possible
        if (self::isMfaAuthorized($token)) {
            self::touchMfaSession($token);
            return;
        }
        $base = defined('APP_BASE') ? APP_BASE : '';
        header('Location: ' . $base . '/ext/' . $token . '/verify');
        exit;
    }

    public static function verify(string $token): void
    {
        $external = self::findExternal($token);
        if (!$external || empty($external['email'])) {
            http_response_code(404);
            view('errors/404', [], 'external');
            exit;
        }

        if (self::isMfaAuthorized($token)) {
            $base = defined('APP_BASE') ? APP_BASE : '';
            header('Location: ' . $base . '/ext/' . $token);
            exit;
        }

        $db     = Database::getInstance();
        $error  = null;
        $resent = false;

        // Auto-send code if none is pending
        $pending = $db->fetch(
            'SELECT id FROM external_mfa_codes WHERE external_id = ? AND expires_at > ? AND used_at IS NULL ORDER BY id DESC LIMIT 1',
            [$external['id'], time()]
        );
        if (!$pending) {
            self::sendNewCode($db, $external);
            $resent = true;
        }

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $postToken = $_POST['ext_token'] ?? '';
            if (!hash_equals($token, $postToken)) {
                $error = t('invalid_request');
            } else {
                $action = $_POST['action'] ?? '';

                if ($action === 'verify_code') {
                    $code = trim($_POST['mfa_code'] ?? '');
                    $row  = $db->fetch(
                        'SELECT * FROM external_mfa_codes WHERE external_id = ? AND expires_at > ? AND used_at IS NULL ORDER BY id DESC LIMIT 1',
                        [$external['id'], time()]
                    );
                    if ($row && password_verify($code, $row['code_hash'])) {
                        $db->execute('UPDATE external_mfa_codes SET used_at = ? WHERE id = ?', [time(), $row['id']]);
                        self::touchMfaSession($token);
                        $base = defined('APP_BASE') ? APP_BASE : '';
                        header('Location: ' . $base . '/ext/' . $token);
                        exit;
                    }
                    if ($row) {
                        $newAttempts = (int)$row['attempts'] + 1;
                        if ($newAttempts >= 5) {
                            // Invalidate code after too many failures; user must request a new one
                            $db->execute('UPDATE external_mfa_codes SET used_at = ? WHERE id = ?', [time(), $row['id']]);
                        } else {
                            $db->execute('UPDATE external_mfa_codes SET attempts = ? WHERE id = ?', [$newAttempts, $row['id']]);
                        }
                    }
                    $error = t('email_mfa_invalid');
                }

                if ($action === 'resend_code') {
                    // Rate limit: one resend per 60 seconds (expires_at was set to time()+1200 on insert)
                    $last = $db->fetch(
                        'SELECT expires_at FROM external_mfa_codes WHERE external_id = ? ORDER BY id DESC LIMIT 1',
                        [$external['id']]
                    );
                    $sentAt = $last ? ((int)$last['expires_at'] - 1200) : 0;
                    if ((time() - $sentAt) < 60) {
                        $error = t('mfa_resend_too_fast');
                    } else {
                        self::sendNewCode($db, $external);
                        $resent = true;
                    }
                }
            }
        }

        view('external/verify', compact('external', 'token', 'error', 'resent'), 'external');
    }

    public static function overview(string $token): void
    {
        $external = self::findExternal($token);
        if (!$external) {
            http_response_code(404);
            view('errors/404', [], 'external');
            exit;
        }

        self::requireMfa($token, $external);

        $db = Database::getInstance();
        $tasks = $db->fetchAll(
            'SELECT t.*, ts.name AS status_name, ts.color AS status_color, ts.is_done
             FROM tasks t
             JOIN task_assignees ta ON ta.task_id = t.id AND ta.external_id = ?
             LEFT JOIN task_statuses ts ON ts.id = t.status_id
             WHERE t.project_id = ?
             ORDER BY t.due_date ASC',
            [$external['id'], $external['project_id']]
        );

        view('external/overview', compact('external', 'tasks', 'token'), 'external');
    }

    public static function task(string $token, int $taskId): void
    {
        $external = self::findExternal($token);
        if (!$external) {
            http_response_code(404);
            view('errors/404', [], 'external');
            exit;
        }

        self::requireMfa($token, $external);

        $db = Database::getInstance();
        $assigned = $db->fetch(
            'SELECT 1 FROM task_assignees WHERE task_id = ? AND external_id = ?',
            [$taskId, $external['id']]
        );
        if (!$assigned) {
            http_response_code(403);
            echo '<!DOCTYPE html><html><body><h1>Zugriff verweigert</h1></body></html>';
            exit;
        }

        $task = $db->fetch(
            'SELECT t.*, ts.name AS status_name, ts.color AS status_color, ts.is_done,
                    p.name AS project_name
             FROM tasks t
             LEFT JOIN task_statuses ts ON ts.id = t.status_id
             JOIN projects p ON p.id = t.project_id
             WHERE t.id = ? AND t.project_id = ?',
            [$taskId, $external['project_id']]
        );
        if (!$task) { http_response_code(404); exit; }

        $statuses = $db->fetchAll(
            'SELECT * FROM task_statuses WHERE project_id = ? ORDER BY position',
            [$external['project_id']]
        );

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

        $error   = null;
        $success = null;

        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $postToken = $_POST['ext_token'] ?? '';
            if (!hash_equals($token, $postToken)) {
                $error = t('invalid_request');
            } else {
                $action = $_POST['action'] ?? '';

                $project = $db->fetch('SELECT * FROM projects WHERE id = ?', [$external['project_id']]);
                $actor   = ['username' => $external['name'], 'email' => $external['email'] ?? ''];

                if ($action === 'update_status') {
                    $newStatusId = (int)($_POST['status_id'] ?? 0);
                    $valid = $db->fetch(
                        'SELECT id, is_done FROM task_statuses WHERE id = ? AND project_id = ?',
                        [$newStatusId, $external['project_id']]
                    );
                    if ($valid) {
                        $db->execute(
                            'UPDATE tasks SET status_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?',
                            [$newStatusId, $taskId]
                        );
                        $task = $db->fetch(
                            'SELECT t.*, ts.name AS status_name, ts.color AS status_color, ts.is_done, p.name AS project_name
                             FROM tasks t LEFT JOIN task_statuses ts ON ts.id = t.status_id
                             JOIN projects p ON p.id = t.project_id WHERE t.id = ?',
                            [$taskId]
                        );
                        Helper::logActivity(null, 'activity_status_changed', $task['title'], $external['project_id'], $taskId);
                        Mailer::notifyManagerActivity($task, $project, Lang::get('activity_status_changed'), $actor);
                        if (!empty($valid['is_done'])) {
                            Mailer::notifyManagerTaskDone($task, $project, $actor);
                        }
                        $success = t('status_updated');
                    }
                }

                if ($action === 'add_comment') {
                    $content = Helper::sanitizeHtml(trim($_POST['content'] ?? ''));
                    if (empty(trim(strip_tags($content)))) {
                        $error = t('comment_empty_error');
                    } elseif (mb_strlen($content) > 5000) {
                        $error = t('comment_too_long');
                    } else {
                        $db->execute(
                            'INSERT INTO comments (task_id, user_id, external_id, external_name, content)
                             VALUES (?, NULL, ?, ?, ?)',
                            [$taskId, $external['id'], $external['name'], $content]
                        );
                        Helper::logActivity(null, 'activity_comment', $task['title'], $external['project_id'], $taskId);
                        Mailer::notifyManagerActivity($task, $project, Lang::get('activity_comment'), $actor);
                        Mailer::notifyAssigneesOnComment($task, $project, ['username' => $external['name'], 'email' => $external['email'] ?? '']);
                        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, $actor);
                        }
                        $success = t('comment_saved');
                        $comments = $db->fetchAll(
                            'SELECT c.*, u.username AS user_name, u.avatar
                             FROM comments c LEFT JOIN users u ON u.id = c.user_id
                             WHERE c.task_id = ? ORDER BY c.created_at ASC',
                            [$taskId]
                        );
                    }
                }
            }
        }

        $mentionData = [];
        $mUsers = $db->fetchAll(
            'SELECT u.username as name FROM project_members pm JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? AND u.is_active = 1',
            [$external['project_id']]
        );
        foreach ($mUsers as $u) $mentionData[] = ['key' => $u['name'], 'display' => $u['name'], 'type' => 'user'];
        $mExts = $db->fetchAll('SELECT name FROM project_externals WHERE project_id = ?', [$external['project_id']]);
        foreach ($mExts as $e) $mentionData[] = ['key' => $e['name'], 'display' => $e['name'], 'type' => 'ext'];

        view('external/task', compact('external', 'task', 'statuses', 'comments', 'token', 'error', 'success', 'mentionData'), 'external');
    }
}
