<?php

declare(strict_types=1);

require_once __DIR__ . '/../db.php';
require_once __DIR__ . '/../auth.php';

header('Content-Type: application/json; charset=utf-8');

function jsonResponse(array $data, int $status = 200): void
{
    http_response_code($status);
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    exit;
}

function getInputData(): array
{
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';

    if (str_contains($contentType, 'application/json')) {
        $raw = file_get_contents('php://input');
        $data = json_decode($raw, true);

        return is_array($data) ? $data : [];
    }

    return $_POST;
}

function requireProjectAccess(string $projectId, int $userId): void
{
    requireProjectAccessForUser($userId, $projectId);
}

function requireTaskAccess(string $taskId, int $userId): void
{
    $stmt = db()->prepare(
        'SELECT tasks.project
         FROM tasks
         INNER JOIN projects ON projects.id = tasks.project
         WHERE tasks.id = ?
         LIMIT 1'
    );

    $stmt->execute([$taskId]);
    $projectId = $stmt->fetchColumn();

    if (!$projectId) {
        jsonResponse([
            'success' => false,
            'error' => 'Task not found.'
        ], 404);
    }

    requireProjectAccessForUser($userId, (string)$projectId);
}

function optionResponse(?array $option): ?array
{
    if (!$option) {
        return null;
    }

    return [
        'id' => (int)$option['id'],
        'name' => $option['name'],
        'icon' => $option['logo'] !== null
            ? 'data:image/svg+xml;base64,' . base64_encode($option['logo'])
            : null
    ];
}

function stateResponse(?array $state): ?array
{
    if (!$state) {
        return null;
    }

    return [
        'id' => (int)$state['id'],
        'name' => $state['name'],
        'color' => $state['color']
    ];
}

function customFieldResponse(array $field): array
{
    $storedValue = $field['value'] ?? $field['default_value'] ?? null;

    if ($storedValue === null) {
        return [
            'id' => (int)$field['id'],
            'task_type' => (int)$field['task_type'],
            'name' => $field['name'],
            'type' => $field['type'],
            'value' => null,
            'raw_value' => ''
        ];
    }

    $decodedValue = json_decode((string)$storedValue, true);
    $decodeOk = json_last_error() === JSON_ERROR_NONE;
    $rawValue = (string)$storedValue;

    if ($decodeOk) {
        if ($field['type'] === 'json') {
            $rawValue = json_encode($decodedValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } elseif ($field['type'] === 'boolean') {
            $rawValue = $decodedValue ? 'true' : 'false';
        } else {
            $rawValue = (string)$decodedValue;
        }
    }

    return [
        'id' => (int)$field['id'],
        'task_type' => (int)$field['task_type'],
        'name' => $field['name'],
        'type' => $field['type'],
        'value' => $decodeOk ? $decodedValue : null,
        'raw_value' => $rawValue
    ];
}

function normalizeCustomFieldValue(mixed $value, string $type): ?string
{
    $rawValue = trim((string)$value);

    if ($rawValue === '') {
        return null;
    }

    $normalized = match ($type) {
        'int' => filter_var($rawValue, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE),
        'float' => is_numeric($rawValue) ? (float)$rawValue : null,
        'boolean' => match (strtolower($rawValue)) {
            '1', 'true', 'yes', 'on' => true,
            '0', 'false', 'no', 'off' => false,
            default => null
        },
        'date' => preg_match('/^\d{4}-\d{2}-\d{2}$/', $rawValue) ? $rawValue : null,
        'json' => json_decode($rawValue, true),
        default => $rawValue
    };

    if ($type === 'json' && json_last_error() !== JSON_ERROR_NONE) {
        jsonResponse(['success' => false, 'error' => 'Custom field JSON value is invalid.'], 400);
    }

    if ($normalized === null && $type !== 'json') {
        jsonResponse(['success' => false, 'error' => "Custom field value does not match {$type}."], 400);
    }

    return json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

function ensureCustomTaskFieldsTable(): void
{
    db()->exec(
        'CREATE TABLE IF NOT EXISTS custom_task_fields (
            id INT NOT NULL AUTO_INCREMENT,
            task_type INT NOT NULL,
            name VARCHAR(128) NOT NULL,
            type VARCHAR(128) NOT NULL,
            default_value TEXT NULL,
            PRIMARY KEY (id),
            INDEX idx_custom_task_fields_task_type (task_type)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
    );

    $columns = db()->query('SHOW COLUMNS FROM custom_task_fields')->fetchAll(PDO::FETCH_COLUMN);

    if (!in_array('default_value', $columns, true)) {
        db()->exec('ALTER TABLE custom_task_fields ADD COLUMN default_value TEXT NULL');
    }

    if (in_array('value', $columns, true)) {
        db()->exec('UPDATE custom_task_fields SET default_value = `value` WHERE default_value IS NULL');
    }
}

function ensureCustomFieldValuesTable(): void
{
    db()->exec(
        'CREATE TABLE IF NOT EXISTS custom_field_values (
            id INT NOT NULL AUTO_INCREMENT,
            field_id INT NOT NULL,
            task_id VARCHAR(26) NOT NULL,
            `value` TEXT NULL,
            PRIMARY KEY (id),
            INDEX idx_custom_field_values_task (task_id),
            INDEX idx_custom_field_values_field (field_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
    );
}

function pictureDataUri(?string $picture): ?string
{
    if ($picture === null || $picture === '') {
        return null;
    }

    $info = @getimagesizefromstring($picture);
    $mime = is_array($info) && isset($info['mime']) ? $info['mime'] : 'image/png';

    return 'data:' . $mime . ';base64,' . base64_encode($picture);
}

function requireTaskOption(string $table, int $id, string $label): void
{
    if ($id <= 0) {
        jsonResponse([
            'success' => false,
            'error' => "{$label} must be valid."
        ], 400);
    }

    $stmt = db()->prepare("SELECT id FROM {$table} WHERE id = ? LIMIT 1");
    $stmt->execute([$id]);

    if (!$stmt->fetch()) {
        jsonResponse([
            'success' => false,
            'error' => "{$label} not found."
        ], 400);
    }
}

function requireUserExists(?int $userId): void
{
    if ($userId === null) {
        return;
    }

    $stmt = db()->prepare('SELECT id FROM users WHERE id = ? LIMIT 1');
    $stmt->execute([$userId]);

    if (!$stmt->fetch()) {
        jsonResponse([
            'success' => false,
            'error' => 'Assignee not found.'
        ], 400);
    }
}

function requireVersionForProject(?int $versionId, string $projectId): void
{
    if ($versionId === null) {
        return;
    }

    $stmt = db()->prepare(
        'SELECT id
         FROM versions
         WHERE id = ?
           AND project = ?
         LIMIT 1'
    );

    $stmt->execute([$versionId, $projectId]);

    if (!$stmt->fetch()) {
        jsonResponse([
            'success' => false,
            'error' => 'Fix version not found for this project.'
        ], 400);
    }
}

function getTaskProject(string $taskId, int $userId): string
{
    $stmt = db()->prepare(
        'SELECT tasks.project
         FROM tasks
         INNER JOIN projects ON projects.id = tasks.project
         WHERE tasks.id = ?
         LIMIT 1'
    );

    $stmt->execute([$taskId]);
    $projectId = $stmt->fetchColumn();

    if (!$projectId) {
        jsonResponse([
            'success' => false,
            'error' => 'Task not found.'
        ], 404);
    }

    return (string)$projectId;
}

function requireCommentForTask(int $commentId, string $taskId, int $userId): void
{
    if ($commentId <= 0) {
        jsonResponse([
            'success' => false,
            'error' => 'Parent comment must be valid.'
        ], 400);
    }

    $stmt = db()->prepare(
        'SELECT comments.id
         FROM comments
         INNER JOIN tasks ON tasks.id = comments.task_id
         INNER JOIN projects ON projects.id = tasks.project
         WHERE comments.id = ?
           AND comments.task_id = ?
         LIMIT 1'
    );

    $stmt->execute([
        $commentId,
        $taskId
    ]);

    if (!$stmt->fetch()) {
        jsonResponse([
            'success' => false,
            'error' => 'Parent comment not found for this task.'
        ], 400);
    }
}

function requireOwnCommentForTask(int $commentId, string $taskId, int $userId): array
{
    if ($commentId <= 0) {
        jsonResponse([
            'success' => false,
            'error' => 'Comment must be valid.'
        ], 400);
    }

    $stmt = db()->prepare(
        'SELECT comments.id, comments.response_to, comments.task_id, comments.commenter, comments.comment
         FROM comments
         INNER JOIN tasks ON tasks.id = comments.task_id
         INNER JOIN projects ON projects.id = tasks.project
         WHERE comments.id = ?
           AND comments.task_id = ?
           AND comments.commenter = ?
         LIMIT 1'
    );

    $stmt->execute([
        $commentId,
        $taskId,
        $userId
    ]);

    $comment = $stmt->fetch();

    if (!$comment) {
        jsonResponse([
            'success' => false,
            'error' => 'Comment not found or not your comment.'
        ], 404);
    }

    return $comment;
}

function generateTaskId(string $projectId): string
{
    $stmt = db()->prepare(
        'SELECT id
         FROM tasks
         WHERE project = ?
           AND id LIKE ?
           AND SUBSTRING(id, ?) REGEXP "^[0-9]+$"
         ORDER BY CAST(SUBSTRING(id, ?) AS UNSIGNED) DESC
         LIMIT 1'
    );

    $stmt->execute([
        $projectId,
        $projectId . '-%',
        strlen($projectId) + 2,
        strlen($projectId) + 2
    ]);

    $lastId = $stmt->fetchColumn();

    if (!$lastId) {
        return $projectId . '-1';
    }

    $parts = explode('-', $lastId);
    $lastNumber = (int)end($parts);

    return $projectId . '-' . ($lastNumber + 1);
}

function resolveTaskStatus(array $task): ?int
{
    if ($task['status'] !== null) {
        return (int)$task['status'];
    }

    $stmt = db()->prepare(
        'SELECT default_state
         FROM task_types
         WHERE id = ?
         LIMIT 1'
    );
    $stmt->execute([(int)$task['type']]);
    $defaultState = $stmt->fetchColumn();

    if (!$defaultState) {
        return null;
    }

    $stateId = (int)$defaultState;
    $assignedStmt = db()->prepare(
        'SELECT id
         FROM assigned_task_states
         WHERE task_type = ?
           AND state = ?
         LIMIT 1'
    );
    $assignedStmt->execute([(int)$task['type'], $stateId]);

    if (!$assignedStmt->fetch()) {
        return null;
    }

    db()->prepare('UPDATE tasks SET status = ? WHERE id = ?')->execute([$stateId, $task['id']]);

    return $stateId;
}

function getTaskState(?int $stateId): ?array
{
    if ($stateId === null) {
        return null;
    }

    $stmt = db()->prepare(
        'SELECT id, name, color
         FROM task_states
         WHERE id = ?
         LIMIT 1'
    );
    $stmt->execute([$stateId]);

    return stateResponse($stmt->fetch() ?: null);
}

function getTaskTransitions(int $taskType, ?int $status): array
{
    if ($status === null) {
        return [];
    }

    $stmt = db()->prepare(
        'SELECT
            task_state_transitions.id,
            task_state_transitions.from_id,
            task_state_transitions.to_id,
            task_state_transitions.action_name,
            to_state.name AS to_name,
            to_state.color AS to_color
         FROM task_state_transitions
         INNER JOIN assigned_task_states from_assignment
            ON from_assignment.task_type = ?
           AND from_assignment.state = task_state_transitions.from_id
         INNER JOIN assigned_task_states to_assignment
            ON to_assignment.task_type = ?
           AND to_assignment.state = task_state_transitions.to_id
         INNER JOIN task_states to_state ON to_state.id = task_state_transitions.to_id
         WHERE task_state_transitions.from_id = ?
         ORDER BY task_state_transitions.id ASC'
    );
    $stmt->execute([$taskType, $taskType, $status]);

    return array_map(static function (array $transition): array {
        return [
            'id' => (int)$transition['id'],
            'from_id' => (int)$transition['from_id'],
            'to_id' => (int)$transition['to_id'],
            'action_name' => $transition['action_name'],
            'to_state' => [
                'id' => (int)$transition['to_id'],
                'name' => $transition['to_name'],
                'color' => $transition['to_color']
            ]
        ];
    }, $stmt->fetchAll());
}

function getTaskCustomFields(string $taskId, int $taskType): array
{
    ensureCustomTaskFieldsTable();
    ensureCustomFieldValuesTable();

    $stmt = db()->prepare(
        'SELECT
            custom_task_fields.id,
            custom_task_fields.task_type,
            custom_task_fields.name,
            custom_task_fields.type,
            custom_task_fields.default_value,
            custom_field_values.id AS value_id,
            custom_field_values.`value`,
            custom_field_values.id IS NOT NULL AS has_task_value
         FROM custom_task_fields
         LEFT JOIN custom_field_values
            ON custom_field_values.field_id = custom_task_fields.id
           AND custom_field_values.task_id = ?
         WHERE custom_task_fields.task_type = ?
         ORDER BY custom_task_fields.id ASC'
    );
    $stmt->execute([$taskId, $taskType]);

    return array_map(static function (array $field): array {
        $response = customFieldResponse([
            'id' => $field['id'],
            'task_type' => $field['task_type'],
            'name' => $field['name'],
            'type' => $field['type'],
            'value' => $field['has_task_value'] ? $field['value'] : $field['default_value']
        ]);
        $response['value_id'] = $field['value_id'] !== null ? (int)$field['value_id'] : null;
        $response['has_task_value'] = (bool)$field['has_task_value'];

        return $response;
    }, $stmt->fetchAll());
}

function getCustomFieldForTask(int $fieldId, string $taskId): array
{
    ensureCustomTaskFieldsTable();

    $stmt = db()->prepare(
        'SELECT
            custom_task_fields.id,
            custom_task_fields.task_type,
            custom_task_fields.name,
            custom_task_fields.type,
            custom_task_fields.default_value,
            tasks.id AS task_id
         FROM custom_task_fields
         INNER JOIN tasks ON tasks.type = custom_task_fields.task_type
         WHERE custom_task_fields.id = ?
           AND tasks.id = ?
         LIMIT 1'
    );
    $stmt->execute([$fieldId, $taskId]);
    $field = $stmt->fetch();

    if (!$field) {
        jsonResponse(['success' => false, 'error' => 'Custom field not found for this task.'], 404);
    }

    return $field;
}

function getTaskListPaging(): array
{
    $page = max(1, (int)($_GET['page_number'] ?? $_GET['page'] ?? 1));
    $perPage = (int)($_GET['per_page'] ?? 100);
    $perPage = max(1, min(100, $perPage));

    return [$page, $perPage, ($page - 1) * $perPage];
}

function getTaskListOrder(): string
{
    $sort = (string)($_GET['sort'] ?? 'id');
    $direction = strtolower((string)($_GET['direction'] ?? 'asc')) === 'desc' ? 'DESC' : 'ASC';
    $columns = [
        'id' => 'tasks.id',
        'title' => 'tasks.title',
        'type' => 'type_option.name',
        'priority' => 'priority_option.name',
        'assignee' => 'users.name',
        'status' => 'status_state.name'
    ];

    $column = $columns[$sort] ?? $columns['id'];

    return "{$column} {$direction}, tasks.id ASC";
}

function taskListResponse(string $whereSql, array $params): array
{
    [$page, $perPage, $offset] = getTaskListPaging();
    $orderBy = getTaskListOrder();

    $countStmt = db()->prepare(
        "SELECT COUNT(*)
         FROM tasks
         INNER JOIN projects ON projects.id = tasks.project
         {$whereSql}"
    );
    $countStmt->execute($params);
    $total = (int)$countStmt->fetchColumn();

    $stmt = db()->prepare(
        "SELECT
            tasks.id,
            tasks.title,
            tasks.type,
            tasks.priority,
            tasks.assignee,
            users.name AS assignee_name,
            users.picture AS assignee_picture,
            COALESCE(tasks.status, task_types.default_state) AS status,
            status_state.name AS status_name,
            status_state.color AS status_color
         FROM tasks
         INNER JOIN projects ON projects.id = tasks.project
         INNER JOIN task_types ON task_types.id = tasks.type
         LEFT JOIN task_types type_option ON type_option.id = tasks.type
         LEFT JOIN task_priorities priority_option ON priority_option.id = tasks.priority
         LEFT JOIN users ON users.id = tasks.assignee
         LEFT JOIN task_states status_state ON status_state.id = COALESCE(tasks.status, task_types.default_state)
         {$whereSql}
         ORDER BY {$orderBy}
         LIMIT {$perPage} OFFSET {$offset}"
    );
    $stmt->execute($params);

    $tasks = array_map(static function (array $task): array {
        $task['assignee_picture'] = pictureDataUri($task['assignee_picture']);
        $task['status'] = $task['status'] !== null ? (int)$task['status'] : null;
        $task['status_state'] = $task['status'] !== null
            ? [
                'id' => (int)$task['status'],
                'name' => $task['status_name'],
                'color' => $task['status_color']
            ]
            : null;
        unset($task['status_name'], $task['status_color']);

        return $task;
    }, $stmt->fetchAll());

    return [
        'tasks' => $tasks,
        'pagination' => [
            'page' => $page,
            'per_page' => $perPage,
            'total' => $total,
            'total_pages' => max(1, (int)ceil($total / $perPage))
        ]
    ];
}

$user = requireApiAuth();

$api = $_GET['api'] ?? '';

switch ($api) {

    case 'ListTypes': {
        $stmt = db()->query(
            'SELECT id, name, logo
             FROM task_types
             ORDER BY id ASC'
        );

        jsonResponse([
            'success' => true,
            'types' => array_map('optionResponse', $stmt->fetchAll())
        ]);
    }

    case 'ListPriorities': {
        $stmt = db()->query(
            'SELECT id, name, logo
             FROM task_priorities
             ORDER BY id ASC'
        );

        jsonResponse([
            'success' => true,
            'priorities' => array_map('optionResponse', $stmt->fetchAll())
        ]);
    }

    case 'TaskInfo': {
        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        $stmt = db()->prepare(
            'SELECT
                tasks.id,
                tasks.title,
                tasks.description,
                tasks.project,
                tasks.created_date,
                tasks.last_changed,
                tasks.reporter,
                tasks.assignee,
                tasks.fix_version,
                tasks.type,
                tasks.priority,
                tasks.status
             FROM tasks
             INNER JOIN projects ON projects.id = tasks.project
             WHERE tasks.id = ?
             LIMIT 1'
        );

        $stmt->execute([$taskId]);

        $task = $stmt->fetch();

        if (!$task) {
            jsonResponse([
                'success' => false,
                'error' => 'Task not found.'
            ], 404);
        }

        $task['status'] = resolveTaskStatus($task);

        jsonResponse([
            'success' => true,
            'task' => [
                ...$task,
                'status_state' => getTaskState($task['status']),
                'status_transitions' => getTaskTransitions((int)$task['type'], $task['status']),
                'custom_fields' => getTaskCustomFields((string)$task['id'], (int)$task['type'])
            ]
        ]);
    }

    case 'ListTasksByProject': {
        $projectId = strtoupper(trim($_GET['project_id'] ?? ''));

        if ($projectId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing project_id.'
            ], 400);
        }

        requireProjectAccess($projectId, (int)$user['id']);

        $result = taskListResponse(
            'WHERE tasks.project = ?',
            [$projectId]
        );

        jsonResponse([
            'success' => true,
            'tasks' => $result['tasks'],
            'pagination' => $result['pagination']
        ]);
    }

    case 'ListTasksByVersion': {
        $versionId = (int)($_GET['version_id'] ?? 0);

        if ($versionId <= 0) {
            jsonResponse([
                'success' => false,
                'error' => 'Missing or invalid version_id.'
            ], 400);
        }

        $versionProjectStmt = db()->prepare(
            'SELECT project
             FROM versions
             WHERE id = ?
             LIMIT 1'
        );
        $versionProjectStmt->execute([$versionId]);
        $versionProject = $versionProjectStmt->fetchColumn();

        if (!$versionProject) {
            jsonResponse([
                'success' => false,
                'error' => 'Version not found.'
            ], 404);
        }

        requireProjectAccessForUser((int)$user['id'], (string)$versionProject);

        $result = taskListResponse(
            'WHERE tasks.fix_version = ?',
            [$versionId]
        );

        jsonResponse([
            'success' => true,
            'tasks' => $result['tasks'],
            'pagination' => $result['pagination']
        ]);
    }

    case 'ListComments': {
        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        requireTaskAccess($taskId, (int)$user['id']);

        $stmt = db()->prepare(
            'SELECT
                comments.id,
                comments.response_to,
                comments.task_id,
                comments.commenter,
                comments.comment,
                users.name AS commenter_name,
                users.email AS commenter_email,
                users.picture AS commenter_picture
             FROM comments
             INNER JOIN users ON users.id = comments.commenter
             WHERE comments.task_id = ?
             ORDER BY comments.id ASC'
        );

        $stmt->execute([$taskId]);

        $comments = array_map(static function (array $comment): array {
            $comment['id'] = (int)$comment['id'];
            $comment['response_to'] = $comment['response_to'] !== null
                ? (int)$comment['response_to']
                : null;
            $comment['commenter'] = (int)$comment['commenter'];
            $comment['commenter_picture'] = pictureDataUri($comment['commenter_picture']);

            return $comment;
        }, $stmt->fetchAll());

        jsonResponse([
            'success' => true,
            'comments' => $comments
        ]);
    }

    case 'Create': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'Create requires POST.'
            ], 405);
        }

        $data = getInputData();

        $projectId = strtoupper(trim($data['project_id'] ?? $data['project'] ?? ''));
        $title = trim($data['title'] ?? '');
        $description = array_key_exists('description', $data) ? trim((string)$data['description']) : null;

        $assignee = isset($data['assignee']) && $data['assignee'] !== ''
            ? (int)$data['assignee']
            : null;

        $fixVersion = isset($data['fix_version']) && $data['fix_version'] !== ''
            ? (int)$data['fix_version']
            : null;

        $type = (int)($data['type'] ?? 0);
        $priority = (int)($data['priority'] ?? 0);

        if ($projectId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Project id is required.'
            ], 400);
        }

        requireProjectRight((int)$user['id'], $projectId, 'Create Tasks');

        if ($title === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Task title is required.'
            ], 400);
        }

        if ($type <= 0) {
            jsonResponse([
                'success' => false,
                'error' => 'Task type is required.'
            ], 400);
        }

        if ($priority <= 0) {
            jsonResponse([
                'success' => false,
                'error' => 'Task priority is required.'
            ], 400);
        }

        requireTaskOption('task_types', $type, 'Task type');
        requireTaskOption('task_priorities', $priority, 'Task priority');
        requireUserExists($assignee);
        requireVersionForProject($fixVersion, $projectId);

        $taskId = generateTaskId($projectId);

        $stmt = db()->prepare(
            'INSERT INTO tasks
             (
                id,
                title,
                description,
                project,
                created_date,
                last_changed,
                reporter,
                assignee,
                fix_version,
                type,
                priority
             )
             VALUES
             (
                ?,
                ?,
                ?,
                ?,
                CURDATE(),
                CURDATE(),
                ?,
                ?,
                ?,
                ?,
                ?
             )'
        );

        $stmt->execute([
            $taskId,
            $title,
            $description !== '' ? $description : null,
            $projectId,
            (int)$user['id'],
            $assignee,
            $fixVersion,
            $type,
            $priority
        ]);

        jsonResponse([
            'success' => true,
            'task_id' => $taskId
        ], 201);
    }

    case 'CreateComment': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'CreateComment requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        requireTaskAccess($taskId, (int)$user['id']);

        $data = getInputData();
        $comment = trim((string)($data['comment'] ?? ''));
        $responseTo = isset($data['response_to']) && $data['response_to'] !== ''
            ? (int)$data['response_to']
            : null;

        if ($comment === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Comment cannot be empty.'
            ], 400);
        }

        if ($responseTo !== null) {
            requireCommentForTask($responseTo, $taskId, (int)$user['id']);
        }

        $stmt = db()->prepare(
            'INSERT INTO comments (response_to, task_id, commenter, comment)
             VALUES (?, ?, ?, ?)'
        );

        $stmt->execute([
            $responseTo,
            $taskId,
            (int)$user['id'],
            $comment
        ]);

        jsonResponse([
            'success' => true,
            'comment_id' => (int)db()->lastInsertId()
        ], 201);
    }

    case 'EditComment': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'EditComment requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));
        $commentId = (int)($_GET['comment_id'] ?? 0);

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        requireOwnCommentForTask($commentId, $taskId, (int)$user['id']);

        $data = getInputData();
        $comment = trim((string)($data['comment'] ?? ''));

        if ($comment === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Comment cannot be empty.'
            ], 400);
        }

        $stmt = db()->prepare(
            'UPDATE comments
             SET comment = ?
             WHERE id = ?
               AND task_id = ?
               AND commenter = ?'
        );

        $stmt->execute([
            $comment,
            $commentId,
            $taskId,
            (int)$user['id']
        ]);

        jsonResponse([
            'success' => true,
            'comment_id' => $commentId
        ]);
    }

    case 'DeleteComment': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'DeleteComment requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));
        $commentId = (int)($_GET['comment_id'] ?? 0);

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        $comment = requireOwnCommentForTask($commentId, $taskId, (int)$user['id']);
        $parentId = $comment['response_to'] !== null ? (int)$comment['response_to'] : null;

        db()->beginTransaction();

        try {
            $childStmt = db()->prepare(
                'UPDATE comments
                 SET response_to = ?
                 WHERE task_id = ?
                   AND response_to = ?'
            );
            $childStmt->execute([
                $parentId,
                $taskId,
                $commentId
            ]);

            $deleteStmt = db()->prepare(
                'DELETE FROM comments
                 WHERE id = ?
                   AND task_id = ?
                   AND commenter = ?'
            );
            $deleteStmt->execute([
                $commentId,
                $taskId,
                (int)$user['id']
            ]);

            db()->commit();

        } catch (Throwable $e) {
            db()->rollBack();

            jsonResponse([
                'success' => false,
                'error' => 'Could not delete comment.'
            ], 500);
        }

        jsonResponse([
            'success' => true,
            'comment_id' => $commentId
        ]);
    }

    case 'SetCustomFieldValue': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'SetCustomFieldValue requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        requireTaskAccess($taskId, (int)$user['id']);
        $taskProject = getTaskProject($taskId, (int)$user['id']);
        requireProjectRight((int)$user['id'], $taskProject, 'Edit Tasks');

        $data = getInputData();
        $fieldId = (int)($data['field_id'] ?? 0);
        $field = getCustomFieldForTask($fieldId, $taskId);
        $value = normalizeCustomFieldValue($data['value'] ?? '', (string)$field['type']);

        ensureCustomFieldValuesTable();

        $existingStmt = db()->prepare(
            'SELECT id
             FROM custom_field_values
             WHERE field_id = ?
               AND task_id = ?
             ORDER BY id ASC
             LIMIT 1'
        );
        $existingStmt->execute([$fieldId, $taskId]);
        $existingId = $existingStmt->fetchColumn();

        if ($existingId) {
            $stmt = db()->prepare('UPDATE custom_field_values SET `value` = ? WHERE id = ?');
            $stmt->execute([$value, $existingId]);
        } else {
            $stmt = db()->prepare(
                'INSERT INTO custom_field_values (field_id, task_id, `value`)
                 VALUES (?, ?, ?)'
            );
            $stmt->execute([$fieldId, $taskId, $value]);
        }

        db()->prepare('UPDATE tasks SET last_changed = CURDATE() WHERE id = ?')->execute([$taskId]);

        jsonResponse([
            'success' => true,
            'field_id' => $fieldId,
            'task_id' => $taskId
        ]);
    }

    case 'TransitionStatus': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'TransitionStatus requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        requireTaskAccess($taskId, (int)$user['id']);

        $stmt = db()->prepare(
            'SELECT id, type, assignee, status
             FROM tasks
             WHERE id = ?
             LIMIT 1'
        );
        $stmt->execute([$taskId]);
        $task = $stmt->fetch();

        if (!$task) {
            jsonResponse(['success' => false, 'error' => 'Task not found.'], 404);
        }

        $canTransition = userIsAdmin((int)$user['id'])
            || userHasRight((int)$user['id'], 'Edit Tasks')
            || (int)$task['assignee'] === (int)$user['id'];

        if (!$canTransition) {
            jsonResponse(['success' => false, 'error' => 'Only the current assignee or users with Edit Tasks can transition this task.'], 403);
        }

        $currentStatus = resolveTaskStatus($task);

        if ($currentStatus === null) {
            jsonResponse(['success' => false, 'error' => 'Task has no current status.'], 400);
        }

        $data = getInputData();
        $transitionId = (int)($data['transition_id'] ?? 0);

        if ($transitionId <= 0) {
            jsonResponse(['success' => false, 'error' => 'Transition must be valid.'], 400);
        }

        $transitionStmt = db()->prepare(
            'SELECT task_state_transitions.id, task_state_transitions.from_id, task_state_transitions.to_id
             FROM task_state_transitions
             INNER JOIN assigned_task_states from_assignment
                ON from_assignment.task_type = ?
               AND from_assignment.state = task_state_transitions.from_id
             INNER JOIN assigned_task_states to_assignment
                ON to_assignment.task_type = ?
               AND to_assignment.state = task_state_transitions.to_id
             WHERE task_state_transitions.id = ?
               AND task_state_transitions.from_id = ?
             LIMIT 1'
        );
        $transitionStmt->execute([
            (int)$task['type'],
            (int)$task['type'],
            $transitionId,
            $currentStatus
        ]);
        $transition = $transitionStmt->fetch();

        if (!$transition) {
            jsonResponse(['success' => false, 'error' => 'Transition is not valid for this task.'], 400);
        }

        db()->prepare('UPDATE tasks SET status = ?, last_changed = CURDATE() WHERE id = ?')->execute([
            (int)$transition['to_id'],
            $taskId
        ]);

        jsonResponse([
            'success' => true,
            'task_id' => $taskId,
            'status' => (int)$transition['to_id']
        ]);
    }

    case 'Edit': {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            jsonResponse([
                'success' => false,
                'error' => 'Edit requires POST.'
            ], 405);
        }

        $taskId = strtoupper(trim($_GET['task_id'] ?? ''));

        if ($taskId === '') {
            jsonResponse([
                'success' => false,
                'error' => 'Missing task_id.'
            ], 400);
        }

        $taskProject = getTaskProject($taskId, (int)$user['id']);
        requireProjectRight((int)$user['id'], $taskProject, 'Edit Tasks');

        $data = getInputData();

        $fields = [];
        $values = [];

        if (array_key_exists('title', $data)) {
            $title = trim((string)$data['title']);

            if ($title === '') {
                jsonResponse([
                    'success' => false,
                    'error' => 'Task title cannot be empty.'
                ], 400);
            }

            $fields[] = 'title = ?';
            $values[] = $title;
        }

        if (array_key_exists('description', $data)) {
            $description = trim((string)$data['description']);

            $fields[] = 'description = ?';
            $values[] = $description !== '' ? $description : null;
        }

        if (array_key_exists('assignee', $data)) {
            $assignee = $data['assignee'] !== '' && $data['assignee'] !== null
                ? (int)$data['assignee']
                : null;

            requireUserExists($assignee);

            $fields[] = 'assignee = ?';
            $values[] = $assignee;
        }

        if (array_key_exists('fix_version', $data)) {
            $fixVersion = $data['fix_version'] !== '' && $data['fix_version'] !== null
                ? (int)$data['fix_version']
                : null;

            requireVersionForProject($fixVersion, $taskProject);

            $fields[] = 'fix_version = ?';
            $values[] = $fixVersion;
        }

        if (array_key_exists('type', $data)) {
            $type = (int)$data['type'];

            if ($type <= 0) {
                jsonResponse([
                    'success' => false,
                    'error' => 'Task type must be valid.'
                ], 400);
            }

            requireTaskOption('task_types', $type, 'Task type');

            $defaultStateStmt = db()->prepare(
                'SELECT default_state
                 FROM task_types
                 WHERE id = ?
                 LIMIT 1'
            );
            $defaultStateStmt->execute([$type]);
            $defaultState = $defaultStateStmt->fetchColumn();

            $fields[] = 'type = ?';
            $values[] = $type;
            $fields[] = 'status = ?';
            $values[] = $defaultState ? (int)$defaultState : null;

            ensureCustomFieldValuesTable();
            db()->prepare('DELETE FROM custom_field_values WHERE task_id = ?')->execute([$taskId]);
        }

        if (array_key_exists('priority', $data)) {
            $priority = (int)$data['priority'];

            if ($priority <= 0) {
                jsonResponse([
                    'success' => false,
                    'error' => 'Task priority must be valid.'
                ], 400);
            }

            requireTaskOption('task_priorities', $priority, 'Task priority');

            $fields[] = 'priority = ?';
            $values[] = $priority;
        }

        if (empty($fields)) {
            jsonResponse([
                'success' => false,
                'error' => 'No editable fields provided.'
            ], 400);
        }

        $fields[] = 'last_changed = CURDATE()';

        $values[] = $taskId;

        $stmt = db()->prepare(
            'UPDATE tasks
             SET ' . implode(', ', $fields) . '
             WHERE id = ?'
        );

        $stmt->execute($values);

        jsonResponse([
            'success' => true,
            'task_id' => $taskId
        ]);
    }
    case 'GetType': {
        $typeId = (int)($_GET['type_id'] ?? 0);

        if ($typeId <= 0) {
            jsonResponse([
                'success' => false,
                'error' => 'Missing or invalid type_id.'
            ], 400);
        }

        $stmt = db()->prepare(
            'SELECT id, name, logo
            FROM task_types
            WHERE id = ?
            LIMIT 1'
        );

        $stmt->execute([$typeId]);
        $type = $stmt->fetch();

        if (!$type) {
            jsonResponse([
                'success' => false,
                'error' => 'Task type not found.'
            ], 404);
        }

        jsonResponse([
            'success' => true,
            'type' => optionResponse($type)
        ]);
    }

    case 'GetPriority': {
        $priorityId = (int)($_GET['priority_id'] ?? 0);

        if ($priorityId <= 0) {
            jsonResponse([
                'success' => false,
                'error' => 'Missing or invalid priority_id.'
            ], 400);
        }

        $stmt = db()->prepare(
            'SELECT id, name, logo
            FROM task_priorities
            WHERE id = ?
            LIMIT 1'
        );

        $stmt->execute([$priorityId]);
        $priority = $stmt->fetch();

        if (!$priority) {
            jsonResponse([
                'success' => false,
                'error' => 'Task priority not found.'
            ], 404);
        }

        jsonResponse([
            'success' => true,
            'priority' => optionResponse($priority)
        ]);
    }

    default: {
        jsonResponse([
            'success' => false,
            'error' => 'Unknown API action.'
        ], 404);
    }
}
