async function apiGet(path, params = {}) {
    const url = new URL(path, window.location.origin);

    Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== '') {
            url.searchParams.set(key, value);
        }
    });

    const response = await fetch(url.toString(), {
        cache: 'no-store',
        headers: {
            'Cache-Control': 'no-cache'
        }
    });
    return response.json();
}

async function apiPost(path, params = {}, body = {}) {
    const url = new URL(path, window.location.origin);

    Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== '') {
            url.searchParams.set(key, value);
        }
    });

    const response = await fetch(url.toString(), {
        method: 'POST',
        cache: 'no-store',
        headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'no-cache'
        },
        body: JSON.stringify(body)
    });

    const text = await response.text();

    try {
        return JSON.parse(text);
    } catch (error) {
        console.error('API did not return JSON:', text);

        return {
            success: false,
            error: 'API did not return JSON. Check console for PHP error.'
        };
    }
}

async function apiPostForm(path, params = {}, formData) {
    const url = new URL(path, window.location.origin);

    Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== '') {
            url.searchParams.set(key, value);
        }
    });

    const response = await fetch(url.toString(), {
        method: 'POST',
        cache: 'no-store',
        headers: {
            'Cache-Control': 'no-cache'
        },
        body: formData
    });

    const text = await response.text();

    try {
        return JSON.parse(text);
    } catch (error) {
        console.error('API did not return JSON:', text);

        return {
            success: false,
            error: 'API did not return JSON. Check console for PHP error.'
        };
    }
}

let currentTask = null;
let currentVersion = null;
let currentProject = null;
let currentProfileUser = null;
let currentVersionTasks = [];
let currentProjectTasks = [];
let currentProjectTaskProject = null;
let currentTaskComments = [];
let workflowData = null;
let workflowGraph = null;
let selectedWorkflowTaskType = null;
let adminOptionsData = null;
let selectedAdminTaskType = null;
let selectedAdminUser = null;
let projectTreePromise = null;

const versionTaskSort = {
    field: 'id',
    direction: 'asc'
};

const projectTaskSort = {
    field: 'id',
    direction: 'asc'
};

const taskTablePageSize = 100;
let versionTaskPagination = null;
let projectTaskPagination = null;

const themeStyles = {
    white: 'app/css/white_mode.css',
    dark: 'app/css/dark_mode.css',
    purple: 'app/css/purple_mode.css',
    green: 'app/css/green_mode.css',
    beige: 'app/css/beige_mode.css'
};

const customFieldTypes = [
    { id: 'string', name: 'String' },
    { id: 'int', name: 'Int' },
    { id: 'float', name: 'Float' },
    { id: 'date', name: 'Date' },
    { id: 'boolean', name: 'Boolean' },
    { id: 'text', name: 'Text' },
    { id: 'json', name: 'JSON' }
];

const taskLookupCache = {
    types: null,
    priorities: null,
    users: null,
    versionsByProject: new Map(),
    projectsById: new Map(),
    projectBlocksById: new Map()
};

function appFlag(name) {
    return document.querySelector('.kiln-app')?.dataset[name] === '1';
}

function canEditTasks() {
    return appFlag('canEditTasks');
}

function canEditVersions() {
    return appFlag('canEditVersions');
}

function canEditProjects() {
    return appFlag('canEditProjects');
}

function applyPermissionVisibility() {
    const visibility = [
        ['taskEditButton', canEditTasks()],
        ['versionEditButton', canEditVersions()],
        ['projectEditButton', canEditProjects()]
    ];

    visibility.forEach(([id, allowed]) => {
        const element = document.getElementById(id);
        if (element) element.hidden = !allowed;
    });

    [
        ['.task-editable', canEditTasks()],
        ['.version-editable', canEditVersions()],
        ['.project-editable', canEditProjects()]
    ].forEach(([selector, allowed]) => {
        document.querySelectorAll(selector).forEach((element) => {
            element.classList.toggle('is-readonly', !allowed);

            if (allowed) {
                element.setAttribute('tabindex', '0');
                element.removeAttribute('aria-disabled');
                return;
            }

            element.removeAttribute('title');
            element.removeAttribute('tabindex');
            element.setAttribute('aria-disabled', 'true');
        });
    });
}

async function loadProjectTree() {
    const tree = document.getElementById('projectTree');

    if (!tree) return;

    tree.innerHTML = '<div class="kiln-tree-muted">Loading projects...</div>';

    const result = await apiGet('/api/project.php', {
        api: 'ListProjects'
    });

    if (!result.success) {
        tree.innerHTML = '<div class="text-danger">Could not load projects.</div>';
        return;
    }

    if (!result.projects.length) {
        tree.innerHTML = '<div class="kiln-tree-muted">No projects yet.</div>';
        return;
    }

    tree.innerHTML = '';
    taskLookupCache.projectBlocksById.clear();

    for (const projectId of result.projects) {
        const project = await getProjectInfo(projectId);
        const projectBlock = document.createElement('div');
        projectBlock.dataset.projectId = projectId;

        const projectButton = document.createElement('button');
        projectButton.className = 'kiln-tree-project';
        projectButton.innerHTML = `
            <i class="fa-solid fa-caret-right me-1" data-project-caret></i>
            <span>${escapeHtml(project?.name ?? projectId)}</span>
        `;
        projectButton.addEventListener('click', (event) => {
            if (event.target.closest('[data-project-caret]')) {
                toggleProject(projectId, projectBlock);
                return;
            }

            loadProject(projectId);

            if (!projectBlock.querySelector('.kiln-tree-group')) {
                toggleProject(projectId, projectBlock, true);
            }
        });

        projectBlock.appendChild(projectButton);
        tree.appendChild(projectBlock);
        taskLookupCache.projectBlocksById.set(projectId, projectBlock);
    }

    if (currentTask?.project) {
        const projectBlock = taskLookupCache.projectBlocksById.get(currentTask.project);

        if (projectBlock && !projectBlock.querySelector('.kiln-tree-group')) {
            await toggleProject(currentTask.project, projectBlock, true);
        }
    }
}

async function getProjectInfo(projectId) {
    if (taskLookupCache.projectsById.has(projectId)) {
        return taskLookupCache.projectsById.get(projectId);
    }

    const result = await apiGet('/api/project.php', {
        api: 'ProjectInfo',
        project_id: projectId
    });

    const project = result.success ? result.project : null;
    taskLookupCache.projectsById.set(projectId, project);
    return project;
}

async function ensureProjectOpen(projectId) {
    await projectTreePromise;

    const projectBlock = taskLookupCache.projectBlocksById.get(projectId);

    if (!projectBlock || projectBlock.querySelector('.kiln-tree-group')) {
        return;
    }

    await toggleProject(projectId, projectBlock, true);
}

async function toggleProject(projectId, projectBlock, forceOpen = false) {
    let group = projectBlock.querySelector('.kiln-tree-group');

    if (group) {
        if (forceOpen) return;

        group.remove();
        setProjectCaret(projectBlock, false);
        return;
    }

    setProjectCaret(projectBlock, true);

    group = document.createElement('div');
    group.className = 'kiln-tree-group';
    group.innerHTML = '<div class="kiln-tree-muted">Loading...</div>';
    projectBlock.appendChild(group);

    const versions = await apiGet('/api/version.php', {
        api: 'ListVersions',
        project_id: projectId
    });

    group.innerHTML = '';

    const versionGroup = document.createElement('div');
    versionGroup.className = 'kiln-tree-version-group';
    group.appendChild(versionGroup);

    const versionLabel = document.createElement('div');
    versionLabel.className = 'kiln-tree-label';
    versionLabel.textContent = 'Versions';
    versionGroup.appendChild(versionLabel);

    if (document.querySelector('.kiln-app')?.dataset.canCreateVersions === '1') {
        const createVersion = document.createElement('button');
        createVersion.className = 'kiln-tree-version';
        createVersion.innerHTML = '<i class="fa-solid fa-plus me-1"></i>Create new version';
        createVersion.addEventListener('click', () => openVersionCreatePopup(projectId));
        versionGroup.appendChild(createVersion);
    }

    if (versions.success && versions.versions.length) {
        for (const versionId of versions.versions) {
            const versionInfo = await apiGet('/api/version.php', {
                api: 'VersionInfo',
                version_id: versionId
            });

            const versionButton = document.createElement('button');
            versionButton.className = 'kiln-tree-version';

            if (versionInfo.success && versionInfo.version) {
                versionButton.textContent = versionInfo.version.name;
            } else {
                versionButton.textContent = `Version ${versionId}`;
            }

            versionButton.addEventListener('click', () => loadVersion(versionId));
            versionGroup.appendChild(versionButton);
        }
    }

    const showTasksButton = document.createElement('button');
    showTasksButton.className = 'kiln-tree-version';
    showTasksButton.innerHTML = '<i class="fa-solid fa-list-check me-1"></i>Show all tasks';
    showTasksButton.addEventListener('click', (event) => {
        event.stopPropagation();
        loadProjectTasks(projectId);
    });

    group.appendChild(showTasksButton);
}

function setProjectCaret(projectBlock, isOpen) {
    const caret = projectBlock.querySelector('[data-project-caret]');

    if (!caret) return;

    caret.classList.toggle('fa-caret-right', !isOpen);
    caret.classList.toggle('fa-caret-down', isOpen);
}

async function loadProject(projectId, pushHistory = true) {
    const view = showView('projectView');

    if (!view) return;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('project', projectId);
        url.searchParams.delete('tasks');
        url.searchParams.delete('task');
        url.searchParams.delete('version');
        url.searchParams.delete('profile');
        url.searchParams.delete('admin');
        window.history.pushState({}, '', url);
    }

    const result = await apiGet('/api/project.php', {
        api: 'ProjectInfo',
        project_id: projectId,
        _: Date.now()
    });

    if (!result.success) {
        view.innerHTML = '<div class="alert alert-danger">Project not found.</div>';
        return;
    }

    await renderProject(result.project);
    await ensureProjectOpen(result.project.id);
}

async function renderProject(project) {
    currentProject = project;
    taskLookupCache.projectsById.set(project.id, project);

    const owner = project.owner ? await getUserInfo(project.owner) : null;

    setVersionEditableText('projectKey', project.id);
    setVersionEditableText('projectName', project.name, project.name);
    setVersionEditableText('projectInlineName', project.name, project.name);
    setText('projectId', project.id);
    setEditableHtml('projectOwner', renderUser(owner), project.owner ?? '', 'No owner');
    setText('projectCreated', project.created_date || '-');
}

async function loadProjectTasks(projectId, page = 1, pushHistory = true) {
    const view = showView('taskListView');

    if (!view) return;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('project', projectId);
        url.searchParams.set('tasks', '1');
        url.searchParams.delete('task');
        url.searchParams.delete('version');
        url.searchParams.delete('profile');
        url.searchParams.delete('admin');
        window.history.pushState({}, '', url);
    }

    currentProjectTaskProject = projectId;
    setText('taskListProjectKey', projectId);
    setText('taskListTitle', `${projectId} Tasks`);

    const container = document.getElementById('taskListContainer');
    const empty = document.getElementById('taskListEmpty');

    container.innerHTML = '<div class="version-task-loading">Loading tasks...</div>';
    empty.hidden = true;
    currentProjectTasks = [];
    projectTaskPagination = null;

    const result = await apiGet('/api/task.php', {
        api: 'ListTasksByProject',
        project_id: projectId,
        page,
        per_page: taskTablePageSize,
        sort: projectTaskSort.field,
        direction: projectTaskSort.direction
    });

    if (!result.success) {
        container.innerHTML = '<div class="alert alert-danger mb-0">Could not load tasks.</div>';
        return;
    }

    if (!result.tasks.length) {
        container.innerHTML = '';
        empty.hidden = false;
        renderTaskTablePagination('project');
        return;
    }

    currentProjectTasks = await normalizeTaskTableRows(result.tasks);
    projectTaskPagination = result.pagination ?? null;
    renderTaskTable('project');
}

async function loadTask(taskId, pushHistory = true) {
    const view = showView('taskView');

    if (!view) return;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('task', taskId);
        url.searchParams.delete('project');
        url.searchParams.delete('tasks');
        url.searchParams.delete('version');
        url.searchParams.delete('profile');
        url.searchParams.delete('admin');
        window.history.pushState({}, '', url);
    }

    const result = await apiGet('/api/task.php', {
        api: 'TaskInfo',
        task_id: taskId,
        _: Date.now()
    });

    if (!result.success) {
        view.innerHTML = '<div class="alert alert-danger">Task not found.</div>';
        return;
    }

    await renderTask(result.task);
    await loadTaskComments(result.task.id);
    await ensureProjectOpen(result.task.project);
}

async function renderTask(task) {
    currentTask = task;

    const [types, priorities, versions, reporter, assignee] = await Promise.all([
        getTaskTypes(),
        getTaskPriorities(),
        getProjectVersions(task.project),
        getUserInfo(task.reporter),
        task.assignee ? getUserInfo(task.assignee) : null
    ]);

    const type = types.find((item) => String(item.id) === String(task.type)) ?? null;
    const priority = priorities.find((item) => String(item.id) === String(task.priority)) ?? null;
    const fixVersion = versions.find((item) => String(item.id) === String(task.fix_version)) ?? null;

    setEditableText('taskKey', task.id);
    setEditableText('taskTitle', task.title, task.title);
    setEditableText('taskDescription', task.description, task.description, 'No description provided.');
    setText('taskProject', task.project);
    document.getElementById('taskReporter').innerHTML = renderUser(reporter);
    setEditableHtml('taskAssignee', renderUser(assignee), task.assignee ?? '', 'Unassigned');
    setEditableText('taskFixVersion', fixVersion ? fixVersion.name : null, task.fix_version ?? '', 'No fix version');
    setText('taskCreated', task.created_date);
    setText('taskUpdated', task.last_changed);

    setEditableHtml('taskType', renderMetaBadge(type), task.type);
    setEditableHtml('taskPriority', renderMetaBadge(priority), task.priority);
    renderTaskStatus(task);
    renderTaskCustomFields(task.custom_fields ?? []);
}

function renderTaskStatus(task) {
    const slot = document.getElementById('taskStatusSlot');

    if (!slot) return;

    slot.innerHTML = '';

    if (!task.status_state) return;

    const isCurrentAssignee = String(task.assignee ?? '') === String(getCurrentUserId());
    const canTransition = (canEditTasks() || appFlag('isAdmin') || isCurrentAssignee)
        && (task.status_transitions ?? []).length > 0;
    const statusStyle = `--task-status-color: ${escapeHtml(task.status_state.color ?? '#6c757d')}`;

    if (!canTransition) {
        slot.innerHTML = `
            <span class="task-status-badge" style="${statusStyle}">
                ${escapeHtml(task.status_state.name)}
            </span>
        `;
        return;
    }

    slot.innerHTML = `
        <div class="task-status-control">
            <button class="task-status-badge task-status-button" type="button" style="${statusStyle}" id="taskStatusButton">
                <span>${escapeHtml(task.status_state.name)}</span>
                <i class="fa-solid fa-chevron-down"></i>
            </button>
            <div class="task-status-menu" id="taskStatusMenu" hidden>
                ${(task.status_transitions ?? []).map((transition) => `
                    <button type="button" data-task-transition="${escapeHtml(transition.id)}">
                        <span>${escapeHtml(transition.action_name)}</span>
                        <small>${escapeHtml(transition.to_state?.name ?? '')}</small>
                    </button>
                `).join('')}
            </div>
        </div>
    `;
}

function renderTaskCustomFields(fields) {
    const panel = document.getElementById('taskCustomFieldsPanel');
    const list = document.getElementById('taskCustomFieldList');

    if (!panel || !list) return;

    const canEdit = canEditTasks();

    panel.hidden = fields.length === 0;
    list.innerHTML = fields.map((field) => `
        <div class="task-custom-field-row">
            <span>${escapeHtml(field.name)}</span>
            <strong
                class="task-custom-field-value ${field.raw_value ? '' : 'is-empty'} ${canEdit ? '' : 'is-readonly'}"
                ${canEdit ? 'tabindex="0"' : 'aria-disabled="true"'}
                data-custom-field-id="${escapeHtml(field.id)}"
                data-custom-field-type="${escapeHtml(field.type)}"
                data-current-value="${escapeHtml(field.raw_value ?? '')}"
            >${field.raw_value ? escapeHtml(field.raw_value) : 'No value'}</strong>
        </div>
    `).join('');
}

async function loadTaskComments(taskId) {
    const list = document.getElementById('taskCommentList');
    const empty = document.getElementById('taskCommentEmpty');

    if (!list || !empty) return;

    list.innerHTML = '';
    empty.hidden = true;
    currentTaskComments = [];
    setText('taskCommentStatus', '');

    const result = await apiGet('/api/task.php', {
        api: 'ListComments',
        task_id: taskId,
        _: Date.now()
    });

    if (!result.success) {
        list.innerHTML = '<div class="alert alert-danger mb-0">Could not load comments.</div>';
        return;
    }

    currentTaskComments = result.comments ?? [];
    renderTaskComments();
}

function renderTaskComments() {
    const list = document.getElementById('taskCommentList');
    const empty = document.getElementById('taskCommentEmpty');

    if (!list || !empty) return;

    list.innerHTML = '';
    empty.hidden = currentTaskComments.length > 0;

    const commentsByParent = groupCommentsByParent(currentTaskComments);
    const renderComment = (comment, depth = 0) => {
        const user = {
            id: comment.commenter,
            name: comment.commenter_name,
            email: comment.commenter_email,
            picture: comment.commenter_picture
        };
        const article = document.createElement('article');
        const canManageComment = String(comment.commenter) === String(getCurrentUserId());
        const manageActions = canManageComment
            ? `
                <button class="task-comment-action" type="button" data-comment-edit="${escapeHtml(comment.id)}">Edit</button>
                <button class="task-comment-action text-danger" type="button" data-comment-delete="${escapeHtml(comment.id)}">Delete</button>
            `
            : '';
        article.className = `task-comment${comment.response_to ? ' is-reply' : ''}`;
        article.style.setProperty('--comment-depth', String(Math.min(depth, 8)));
        article.dataset.commentId = comment.id;
        article.dataset.commentText = comment.comment;
        article.innerHTML = `
            <div class="task-comment-header">
                ${renderUser(user)}
                <span class="task-comment-id">#${escapeHtml(comment.id)}</span>
            </div>
            <div class="task-comment-body">${escapeHtml(comment.comment)}</div>
            <div class="task-comment-action-row">
                <button class="task-comment-action" type="button" data-comment-reply="${escapeHtml(comment.id)}">Reply</button>
                ${manageActions}
            </div>
            <div class="task-comment-reply-slot"></div>
            <div class="task-comment-edit-slot"></div>
        `;

        list.appendChild(article);

        const children = commentsByParent.get(Number(comment.id)) ?? [];
        children.forEach((child) => renderComment(child, depth + 1));
    };

    const rootComments = commentsByParent.get(null) ?? [];
    rootComments.forEach((comment) => renderComment(comment));
}

function groupCommentsByParent(comments) {
    const knownIds = new Set(comments.map((comment) => Number(comment.id)));
    const groups = new Map([[null, []]]);

    [...comments]
        .sort((first, second) => Number(first.id) - Number(second.id))
        .forEach((comment) => {
            const parentId = comment.response_to && knownIds.has(Number(comment.response_to))
                ? Number(comment.response_to)
                : null;

            if (!groups.has(parentId)) {
                groups.set(parentId, []);
            }

            groups.get(parentId).push(comment);
        });

    return groups;
}

async function submitTaskComment(comment, responseTo = null) {
    if (!currentTask) return;

    const result = await apiPost('/api/task.php', {
        api: 'CreateComment',
        task_id: currentTask.id
    }, {
        comment,
        response_to: responseTo
    });

    if (!result.success) {
        throw new Error(result.error || 'Could not save comment.');
    }

    await loadTaskComments(currentTask.id);
}

async function updateTaskComment(commentId, comment) {
    if (!currentTask) return;

    const result = await apiPost('/api/task.php', {
        api: 'EditComment',
        task_id: currentTask.id,
        comment_id: commentId
    }, {
        comment
    });

    if (!result.success) {
        throw new Error(result.error || 'Could not update comment.');
    }

    await loadTaskComments(currentTask.id);
}

async function deleteTaskComment(commentId) {
    if (!currentTask) return;

    const result = await apiPost('/api/task.php', {
        api: 'DeleteComment',
        task_id: currentTask.id,
        comment_id: commentId
    });

    if (!result.success) {
        throw new Error(result.error || 'Could not delete comment.');
    }

    await loadTaskComments(currentTask.id);
}

function showView(viewId) {
    document.querySelectorAll('#viewer > section').forEach((section) => {
        section.hidden = true;
    });

    const view = document.getElementById(viewId);

    if (!view) {
        console.error(`Missing view: ${viewId}`);
        return null;
    }

    view.hidden = false;
    return view;
}

function setText(id, value) {
    const element = document.getElementById(id);

    if (element) {
        element.textContent = value ?? '';
    }
}

function setEditableText(id, displayValue, rawValue = displayValue, emptyText = '-') {
    const element = document.getElementById(id);

    if (!element) return;

    const hasValue = displayValue !== null && displayValue !== undefined && displayValue !== '';
    element.textContent = hasValue ? displayValue : emptyText;
    element.dataset.currentValue = rawValue ?? '';
    element.classList.remove('is-saving');
    element.classList.toggle('is-empty', !hasValue);
}

function setEditableHtml(id, displayHtml, rawValue, emptyText = '-') {
    const element = document.getElementById(id);

    if (!element) return;

    const hasValue = displayHtml !== null && displayHtml !== undefined && displayHtml !== '-';
    element.innerHTML = hasValue ? displayHtml : emptyText;
    element.dataset.currentValue = rawValue ?? '';
    element.classList.remove('is-saving');
    element.classList.toggle('is-empty', !hasValue || rawValue === '' || rawValue === null);
}

function setVersionEditableText(id, displayValue, rawValue = displayValue, emptyText = '-') {
    const element = document.getElementById(id);

    if (!element) return;

    const hasValue = displayValue !== null && displayValue !== undefined && displayValue !== '';
    element.textContent = hasValue ? displayValue : emptyText;
    element.dataset.currentValue = rawValue ?? '';
    element.classList.remove('is-saving');
    element.classList.toggle('is-empty', !hasValue);
}

function renderMetaBadge(item) {
    if (!item) {
        return '-';
    }

    const icon = item.icon
        ? `<img src="${item.icon}" alt="" class="meta-icon">`
        : '';

    return `
        <span class="meta-badge">
            ${icon}
            <span>${escapeHtml(item.name)}</span>
        </span>
    `;
}

async function getTaskTypes() {
    if (taskLookupCache.types) {
        return taskLookupCache.types;
    }

    const result = await apiGet('/api/task.php', {
        api: 'ListTypes'
    });

    taskLookupCache.types = result.success ? result.types : [];
    return taskLookupCache.types;
}

async function getTaskPriorities() {
    if (taskLookupCache.priorities) {
        return taskLookupCache.priorities;
    }

    const result = await apiGet('/api/task.php', {
        api: 'ListPriorities'
    });

    taskLookupCache.priorities = result.success ? result.priorities : [];
    return taskLookupCache.priorities;
}

async function getUsers() {
    if (taskLookupCache.users) {
        return taskLookupCache.users;
    }

    const result = await apiGet('/api/user.php', {
        api: 'ListUsers'
    });

    taskLookupCache.users = result.success ? result.users : [];
    return taskLookupCache.users;
}

async function getProjectVersions(projectId) {
    if (taskLookupCache.versionsByProject.has(projectId)) {
        return taskLookupCache.versionsByProject.get(projectId);
    }

    const result = await apiGet('/api/version.php', {
        api: 'ListVersions',
        project_id: projectId
    });

    if (!result.success) {
        taskLookupCache.versionsByProject.set(projectId, []);
        return [];
    }

    const versions = [];

    for (const versionId of result.versions) {
        const versionInfo = await apiGet('/api/version.php', {
            api: 'VersionInfo',
            version_id: versionId
        });

        if (versionInfo.success && versionInfo.version) {
            versions.push(versionInfo.version);
        }
    }

    taskLookupCache.versionsByProject.set(projectId, versions);
    return versions;
}

async function populateTaskOptionSelects() {
    const [types, priorities] = await Promise.all([
        getTaskTypes(),
        getTaskPriorities()
    ]);

    populateSelect(document.getElementById('createTaskType'), types, {
        placeholder: 'Select type'
    });

    populateSelect(document.getElementById('createTaskPriority'), priorities, {
        placeholder: 'Select priority'
    });
}

async function populateTaskProjectSelect(selectedProjectId = '') {
    const result = await apiGet('/api/project.php', {
        api: 'ListProjects'
    });

    const projects = [];

    if (result.success) {
        for (const projectId of result.projects) {
            const project = await getProjectInfo(projectId);

            projects.push({
                id: projectId,
                name: project?.name ?? projectId
            });
        }
    }

    populateSelect(document.getElementById('taskFormProjectId'), projects, {
        placeholder: 'Select project',
        selectedValue: selectedProjectId
    });
}

async function populateTaskRelationSelects(projectId = '', selected = {}) {
    const users = await getUsers();

    populateSelect(document.getElementById('taskFormAssignee'), users, {
        placeholder: 'Unassigned',
        selectedValue: selected.assignee ?? ''
    });

    const versions = projectId ? await getProjectVersions(projectId.toUpperCase()) : [];

    populateSelect(document.getElementById('taskFormFixVersion'), versions, {
        placeholder: 'No fix version',
        selectedValue: selected.fix_version ?? ''
    });
}

function resetTaskPopup() {
    const form = document.getElementById('createTaskForm');

    if (!form) return;

    form.reset();
    form.dataset.mode = 'create';
    document.getElementById('taskPopupTitle').textContent = 'Create Task';
    document.getElementById('taskPopupSubmit').textContent = 'Create Task';
    document.getElementById('taskFormTaskId').value = '';
    document.getElementById('taskFormProjectId').disabled = false;
}

function resetVersionPopup() {
    const form = document.getElementById('createVersionForm');

    if (!form) return;

    form.reset();
    form.dataset.mode = 'create';
    document.getElementById('versionPopupTitle').textContent = 'Create Version';
    document.getElementById('versionPopupSubmit').textContent = 'Create Version';
    document.getElementById('versionFormVersionId').value = '';
    document.getElementById('versionFormProjectId').value = '';
}

function populateSelect(select, options, settings = {}) {
    if (!select) return;

    const placeholder = settings.placeholder ?? 'Select value';
    const includeEmpty = settings.includeEmpty ?? true;
    const selectedValue = settings.selectedValue ?? '';

    select.innerHTML = '';

    if (includeEmpty) {
        const option = document.createElement('option');
        option.value = '';
        option.textContent = placeholder;
        select.appendChild(option);
    }

    options.forEach((item) => {
        const option = document.createElement('option');
        option.value = item.id;
        option.textContent = item.name ?? item.email ?? item.id;
        option.selected = String(option.value) === String(selectedValue);
        select.appendChild(option);
    });
}

async function openTaskCreatePopup() {
    const form = document.getElementById('createTaskForm');

    if (!form) return;

    await populateTaskOptionSelects();
    await populateTaskProjectSelect();
    await populateTaskRelationSelects();

    resetTaskPopup();

    openPopup('createTask');
}

async function openTaskEditPopup() {
    if (!currentTask) return;

    const form = document.getElementById('createTaskForm');

    if (!form) return;

    await populateTaskOptionSelects();
    await populateTaskProjectSelect(currentTask.project);
    await populateTaskRelationSelects(currentTask.project, {
        assignee: currentTask.assignee ?? '',
        fix_version: currentTask.fix_version ?? ''
    });

    form.reset();
    form.dataset.mode = 'edit';
    document.getElementById('taskPopupTitle').textContent = `Edit ${currentTask.id}`;
    document.getElementById('taskPopupSubmit').textContent = 'Update Task';
    document.getElementById('taskFormTaskId').value = currentTask.id;
    document.getElementById('taskFormProjectId').value = currentTask.project;
    document.getElementById('taskFormProjectId').disabled = true;
    document.getElementById('taskFormTitle').value = currentTask.title ?? '';
    document.getElementById('taskFormDescription').value = currentTask.description ?? '';
    document.getElementById('createTaskType').value = currentTask.type ?? '';
    document.getElementById('createTaskPriority').value = currentTask.priority ?? '';
    document.getElementById('taskFormFixVersion').value = currentTask.fix_version ?? '';
    document.getElementById('taskFormAssignee').value = currentTask.assignee ?? '';

    openPopup('createTask');
}

function openVersionCreatePopup(projectId) {
    resetVersionPopup();
    document.getElementById('versionFormProjectId').value = projectId;
    openPopup('createVersion');
}

async function populateProjectOwnerSelect(selectedOwner = '') {
    populateSelect(document.getElementById('projectFormOwner'), await getUsers(), {
        includeEmpty: false,
        selectedValue: selectedOwner
    });
}

function resetProjectPopup() {
    const form = document.getElementById('createProjectForm');

    if (!form) return;

    form.reset();
    form.dataset.mode = 'create';
    document.getElementById('projectPopupTitle').textContent = 'Create Project';
    document.getElementById('projectPopupSubmit').textContent = 'Create Project';
    document.getElementById('projectFormId').disabled = false;
    document.getElementById('projectFormId').value = '';
    document.getElementById('projectFormName').value = '';
}

async function openProjectCreatePopup() {
    resetProjectPopup();
    await populateProjectOwnerSelect(getCurrentUserId());
    openPopup('createProject');
}

async function openProjectEditPopup() {
    if (!currentProject) return;

    resetProjectPopup();

    const form = document.getElementById('createProjectForm');

    if (!form) return;

    form.dataset.mode = 'edit';
    document.getElementById('projectPopupTitle').textContent = `Edit ${currentProject.id}`;
    document.getElementById('projectPopupSubmit').textContent = 'Update Project';
    document.getElementById('projectFormId').value = currentProject.id;
    document.getElementById('projectFormId').disabled = true;
    document.getElementById('projectFormName').value = currentProject.name ?? '';
    await populateProjectOwnerSelect(currentProject.owner ?? '');

    openPopup('createProject');
}

function openVersionEditPopup() {
    if (!currentVersion) return;

    resetVersionPopup();

    const form = document.getElementById('createVersionForm');

    if (!form) return;

    form.dataset.mode = 'edit';
    document.getElementById('versionPopupTitle').textContent = `Edit ${currentVersion.name}`;
    document.getElementById('versionPopupSubmit').textContent = 'Update Version';
    document.getElementById('versionFormVersionId').value = currentVersion.id;
    document.getElementById('versionFormProjectId').value = currentVersion.project;
    document.getElementById('versionFormName').value = currentVersion.name ?? '';
    document.getElementById('versionFormDescription').value = currentVersion.description ?? '';
    document.getElementById('versionFormDueDate').value = currentVersion.due_date ?? '';
    document.getElementById('versionFormReleasedDate').value = currentVersion.released_date ?? '';

    openPopup('createVersion');
}

function initTaskInlineEditing() {
    if (!canEditTasks()) return;

    document.querySelectorAll('.task-editable').forEach((element) => {
        if (element.dataset.inlineReady === 'true') return;

        element.dataset.inlineReady = 'true';
        element.title = 'Click to edit';

        element.addEventListener('click', () => openTaskInlineEditor(element));
        element.addEventListener('keydown', (event) => {
            if (event.key === 'Enter' || event.key === ' ') {
                event.preventDefault();
                openTaskInlineEditor(element);
            }
        });
    });
}

async function openTaskInlineEditor(element) {
    if (!canEditTasks() || !currentTask || element.querySelector('.task-inline-form')) return;

    const field = element.dataset.taskField;
    const currentValue = element.dataset.currentValue ?? '';
    const originalHtml = element.innerHTML;
    const originalClasses = Array.from(element.classList);

    element.classList.remove('is-empty');
    element.innerHTML = '';

    const form = document.createElement('form');
    form.className = 'task-inline-form';

    const input = await createTaskInlineInput(field, currentValue);
    form.appendChild(input);

    const actions = document.createElement('div');
    actions.className = 'task-inline-actions';
    actions.innerHTML = `
        <button class="btn btn-sm btn-primary" type="submit">
            <i class="fa-solid fa-check"></i>
        </button>
        <button class="btn btn-sm btn-outline-secondary" type="button" data-inline-cancel>
            <i class="fa-solid fa-xmark"></i>
        </button>
    `;

    if (input.tagName !== 'SELECT') {
        form.appendChild(actions);
    }

    element.appendChild(form);
    input.focus();

    if (input.select && input.tagName !== 'SELECT') {
        input.select();
    }

    let editorClosed = false;

    const restore = () => {
        if (editorClosed) return;

        editorClosed = true;
        element.innerHTML = originalHtml;
        element.className = originalClasses.join(' ');
    };

    let saving = false;

    const save = async () => {
        if (saving) return;

        const nextValue = input.value;

        if (String(nextValue) === String(currentValue)) {
            restore();
            return;
        }

        saving = true;
        element.classList.add('is-saving');

        const result = await apiPost('/api/task.php', {
            api: 'Edit',
            task_id: currentTask.id
        }, {
            [field]: nextValue
        });

        if (!result.success) {
            alert(result.error || 'Could not update task.');
            saving = false;
            restore();
            return;
        }

        editorClosed = true;
        element.classList.remove('is-saving');
        currentTask = {
            ...currentTask,
            [field]: nextValue === '' ? null : nextValue
        };

        if (field === 'type') {
            await loadTask(currentTask.id, false);
            return;
        }

        await renderTask(currentTask);
    };

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        await save();
    });

    form.querySelector('[data-inline-cancel]')?.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        restore();
    });

    input.addEventListener('keydown', async (event) => {
        if (event.key === 'Escape') {
            event.preventDefault();
            restore();
        }

        if (event.key === 'Enter' && input.tagName === 'TEXTAREA' && (event.ctrlKey || event.metaKey)) {
            event.preventDefault();
            await save();
        }
    });

    if (input.tagName === 'SELECT') {
        input.addEventListener('change', save);
        input.addEventListener('blur', () => {
            window.setTimeout(() => {
                if (editorClosed) return;
                if (element.classList.contains('is-saving')) return;
                if (element.contains(document.activeElement)) return;
                restore();
            }, 120);
        });
    }
}

async function createTaskInlineInput(field, currentValue) {
    if (field === 'description') {
        const textarea = document.createElement('textarea');
        textarea.className = 'form-control task-inline-input';
        textarea.rows = 5;
        textarea.value = currentValue;
        return textarea;
    }

    if (field === 'type') {
        const select = document.createElement('select');
        select.className = 'form-select task-inline-select';
        populateSelect(select, await getTaskTypes(), {
            includeEmpty: false,
            selectedValue: currentValue
        });
        return select;
    }

    if (field === 'priority') {
        const select = document.createElement('select');
        select.className = 'form-select task-inline-select';
        populateSelect(select, await getTaskPriorities(), {
            includeEmpty: false,
            selectedValue: currentValue
        });
        return select;
    }

    if (field === 'fix_version') {
        const select = document.createElement('select');
        select.className = 'form-select task-inline-select';
        populateSelect(select, await getProjectVersions(currentTask.project), {
            placeholder: 'No fix version',
            selectedValue: currentValue
        });
        return select;
    }

    if (field === 'assignee') {
        const select = document.createElement('select');
        select.className = 'form-select task-inline-select';
        populateSelect(select, await getUsers(), {
            placeholder: 'Unassigned',
            selectedValue: currentValue
        });
        return select;
    }

    const input = document.createElement('input');
    input.className = 'form-control task-inline-input';
    input.type = 'text';
    input.value = currentValue;
    input.required = field === 'title';
    return input;
}

function createCustomFieldInput(type, currentValue) {
    if (type === 'boolean') {
        const select = document.createElement('select');
        select.className = 'form-select task-inline-select';
        [
            { id: '', name: 'No value' },
            { id: 'true', name: 'True' },
            { id: 'false', name: 'False' }
        ].forEach((item) => {
            const option = document.createElement('option');
            option.value = item.id;
            option.textContent = item.name;
            option.selected = String(item.id) === String(currentValue);
            select.appendChild(option);
        });
        return select;
    }

    if (type === 'text' || type === 'json') {
        const textarea = document.createElement('textarea');
        textarea.className = 'form-control task-inline-input';
        textarea.rows = type === 'json' ? 4 : 3;
        textarea.value = currentValue;
        return textarea;
    }

    const input = document.createElement('input');
    input.className = 'form-control task-inline-input';
    input.type = type === 'date' ? 'date' : 'text';
    input.inputMode = ['int', 'float'].includes(type) ? 'decimal' : '';
    input.value = currentValue;
    return input;
}

async function openCustomFieldInlineEditor(element) {
    if (!canEditTasks() || !currentTask || element.querySelector('.task-inline-form')) return;

    const fieldId = element.dataset.customFieldId;
    const fieldType = element.dataset.customFieldType ?? 'string';
    const currentValue = element.dataset.currentValue ?? '';
    const originalHtml = element.innerHTML;
    const originalClasses = Array.from(element.classList);

    element.classList.remove('is-empty');
    element.innerHTML = '';

    const form = document.createElement('form');
    form.className = 'task-inline-form';

    const input = createCustomFieldInput(fieldType, currentValue);
    form.appendChild(input);

    const actions = document.createElement('div');
    actions.className = 'task-inline-actions';
    actions.innerHTML = `
        <button class="btn btn-sm btn-primary" type="submit">
            <i class="fa-solid fa-check"></i>
        </button>
        <button class="btn btn-sm btn-outline-secondary" type="button" data-inline-cancel>
            <i class="fa-solid fa-xmark"></i>
        </button>
    `;
    form.appendChild(actions);

    element.appendChild(form);
    input.focus();

    if (input.select && input.tagName !== 'SELECT') {
        input.select();
    }

    let closed = false;
    const restore = () => {
        if (closed) return;

        closed = true;
        element.innerHTML = originalHtml;
        element.className = originalClasses.join(' ');
    };

    const save = async () => {
        const nextValue = input.value;

        if (String(nextValue) === String(currentValue)) {
            restore();
            return;
        }

        element.classList.add('is-saving');

        const result = await apiPost('/api/task.php', {
            api: 'SetCustomFieldValue',
            task_id: currentTask.id
        }, {
            field_id: fieldId,
            value: nextValue
        });

        if (!result.success) {
            alert(result.error || 'Could not update custom field.');
            restore();
            return;
        }

        closed = true;
        await loadTask(currentTask.id, false);
    };

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        await save();
    });

    form.querySelector('[data-inline-cancel]')?.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        restore();
    });

    input.addEventListener('keydown', async (event) => {
        if (event.key === 'Escape') {
            event.preventDefault();
            restore();
        }

        if (event.key === 'Enter' && input.tagName !== 'TEXTAREA') {
            event.preventDefault();
            await save();
        }

        if (event.key === 'Enter' && input.tagName === 'TEXTAREA' && (event.ctrlKey || event.metaKey)) {
            event.preventDefault();
            await save();
        }
    });
}

async function transitionCurrentTask(transitionId) {
    if (!currentTask || !transitionId) return;

    const result = await apiPost('/api/task.php', {
        api: 'TransitionStatus',
        task_id: currentTask.id
    }, {
        transition_id: transitionId
    });

    if (!result.success) {
        alert(result.error || 'Could not transition task.');
        return;
    }

    await loadTask(currentTask.id, false);
}

async function loadVersion(versionId, pushHistory = true) {
    const view = showView('versionView');

    if (!view) return;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('version', versionId);
        url.searchParams.delete('task');
        url.searchParams.delete('project');
        url.searchParams.delete('tasks');
        url.searchParams.delete('profile');
        url.searchParams.delete('admin');
        window.history.pushState({}, '', url);
    }

    const result = await apiGet('/api/version.php', {
        api: 'VersionInfo',
        version_id: versionId
    });

    if (!result.success) {
        setText('versionName', 'Version not found');
        setText('versionDescription', '');
        return;
    }

    await renderVersion(result.version);
    await loadVersionTasks(versionId);
    await ensureProjectOpen(result.version.project);
}

async function loadProfile(pushHistory = true) {
    const view = showView('profileView');

    if (!view) return;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('profile', '1');
        url.searchParams.delete('task');
        url.searchParams.delete('project');
        url.searchParams.delete('tasks');
        url.searchParams.delete('version');
        url.searchParams.delete('admin');
        window.history.pushState({}, '', url);
    }

    const userId = getCurrentUserId();
    const result = await apiGet('/api/user.php', {
        api: 'UserInfo',
        user_id: userId,
        _: Date.now()
    });

    if (!result.success) {
        view.innerHTML = '<div class="alert alert-danger">Could not load profile.</div>';
        return;
    }

    renderProfile(result.user);
}

function renderProfile(user) {
    currentProfileUser = user;

    setText('profileStatus', '');
    document.getElementById('profileName').value = user.name ?? '';
    document.getElementById('profileEmail').value = user.email ?? '';
    document.getElementById('profilePassword').value = '';
    document.getElementById('profilePasswordConfirm').value = '';
    document.getElementById('profileTheme').value = user.settings?.theme ?? 'dark';
    document.getElementById('profileRemovePicture').checked = false;
    document.getElementById('profilePictureInput').value = '';
    renderProfileAvatar(user.picture);
}

function renderProfileAvatar(picture) {
    const preview = document.getElementById('profileAvatarPreview');

    if (!preview) return;

    if (picture) {
        preview.innerHTML = `<img src="${picture}" alt="">`;
        return;
    }

    preview.innerHTML = `
        <span class="profile-avatar-fallback">
            <i class="fa-solid fa-user"></i>
        </span>
    `;
}

async function renderVersion(version) {
    currentVersion = version;

    setVersionEditableText('versionKey', `Version ${version.id}`);
    setVersionEditableText('versionName', version.name, version.name);
    setVersionEditableText('versionDescription', version.description, version.description, 'No description provided.');
    setText('versionProject', version.project);
    setText('versionCreated', version.created_date || '-');
    setVersionEditableText('versionDueDate', version.due_date, version.due_date, 'No due date');
    setVersionEditableText('versionReleasedDate', version.released_date, version.released_date, 'Not released');
}

async function loadVersionTasks(versionId, page = 1) {
    const list = document.getElementById('versionTaskList');
    const empty = document.getElementById('versionTaskEmpty');

    if (!list || !empty) return;

    list.innerHTML = '<div class="version-task-loading">Loading tasks...</div>';
    empty.hidden = true;
    currentVersionTasks = [];
    versionTaskPagination = null;

    const result = await apiGet('/api/task.php', {
        api: 'ListTasksByVersion',
        version_id: versionId,
        page,
        per_page: taskTablePageSize,
        sort: versionTaskSort.field,
        direction: versionTaskSort.direction
    });

    if (!result.success) {
        list.innerHTML = '<div class="alert alert-danger mb-0">Could not load tasks.</div>';
        return;
    }

    if (!result.tasks.length) {
        list.innerHTML = '';
        empty.hidden = false;
        renderTaskTablePagination('version');
        return;
    }

    currentVersionTasks = await normalizeTaskTableRows(result.tasks);
    versionTaskPagination = result.pagination ?? null;
    renderTaskTable('version');
}

async function normalizeTaskTableRows(tasks) {
    const [types, priorities] = await Promise.all([
        getTaskTypes(),
        getTaskPriorities()
    ]);

    return tasks.map((task) => {
        const normalizedTask = typeof task === 'string'
            ? { id: task, title: '', type: null, priority: null }
            : task;

        const type = types.find((item) => String(item.id) === String(normalizedTask.type)) ?? null;
        const priority = priorities.find((item) => String(item.id) === String(normalizedTask.priority)) ?? null;

        return {
            ...normalizedTask,
            typeOption: type,
            priorityOption: priority,
            typeName: type?.name ?? '',
            priorityName: priority?.name ?? '',
            statusName: normalizedTask.status_state?.name ?? '',
            statusColor: normalizedTask.status_state?.color ?? '',
            assigneeUser: normalizedTask.assignee
                ? {
                    id: normalizedTask.assignee,
                    name: normalizedTask.assignee_name ?? `User ${normalizedTask.assignee}`,
                    picture: normalizedTask.assignee_picture ?? null
                }
                : null,
            assigneeName: normalizedTask.assignee_name ?? ''
        };
    });
}

function renderTaskTable(kind) {
    const isVersion = kind === 'version';
    const list = document.getElementById(isVersion ? 'versionTaskList' : 'taskListContainer');
    const empty = document.getElementById(isVersion ? 'versionTaskEmpty' : 'taskListEmpty');
    const tasks = isVersion ? currentVersionTasks : currentProjectTasks;

    if (!list || !empty) return;

    updateTaskTableSortButtons(kind);

    list.innerHTML = '';
    empty.hidden = tasks.length > 0;

    tasks.forEach((task) => {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'version-task-row version-task-item';
        button.innerHTML = `
            <span class="version-task-id">${escapeHtml(task.id)}</span>
            <span class="version-task-title">${escapeHtml(task.title || '-')}</span>
            <span>${renderMetaBadge(task.typeOption)}</span>
            <span>${renderMetaBadge(task.priorityOption)}</span>
            <span>${renderUser(task.assigneeUser)}</span>
            <span>${renderTaskTableStatus(task)}</span>
        `;
        button.addEventListener('click', () => loadTask(task.id));

        list.appendChild(button);
    });

    renderTaskTablePagination(kind);
}

function renderTaskTableStatus(task) {
    if (!task.statusName) return '-';

    const color = task.statusColor || '#6c757d';

    return `
        <span class="version-task-status" style="--task-status-color: ${escapeHtml(color)}">
            ${escapeHtml(task.statusName)}
        </span>
    `;
}

function renderTaskTablePagination(kind) {
    const isVersion = kind === 'version';
    const pagination = isVersion ? versionTaskPagination : projectTaskPagination;
    const container = document.getElementById(isVersion ? 'versionTaskPagination' : 'projectTaskPagination');

    if (!container) return;

    if (!pagination || pagination.total <= pagination.per_page) {
        container.innerHTML = '';
        return;
    }

    const start = ((pagination.page - 1) * pagination.per_page) + 1;
    const end = Math.min(pagination.total, pagination.page * pagination.per_page);

    container.innerHTML = `
        <span>${escapeHtml(start)}-${escapeHtml(end)} of ${escapeHtml(pagination.total)}</span>
        <div class="version-task-page-actions">
            <button class="btn btn-sm btn-outline-secondary" type="button" data-task-page="${escapeHtml(kind)}" data-page="${escapeHtml(pagination.page - 1)}" ${pagination.page <= 1 ? 'disabled' : ''}>
                <i class="fa-solid fa-chevron-left"></i>
            </button>
            <button class="btn btn-sm btn-outline-secondary" type="button" data-task-page="${escapeHtml(kind)}" data-page="${escapeHtml(pagination.page + 1)}" ${pagination.page >= pagination.total_pages ? 'disabled' : ''}>
                <i class="fa-solid fa-chevron-right"></i>
            </button>
        </div>
    `;
}

function updateTaskTableSortButtons(kind) {
    const isVersion = kind === 'version';
    const selector = isVersion ? '[data-version-task-sort]' : '[data-project-task-sort]';
    const state = isVersion ? versionTaskSort : projectTaskSort;

    document.querySelectorAll(selector).forEach((button) => {
        const field = isVersion ? button.dataset.versionTaskSort : button.dataset.projectTaskSort;
        const isActive = field === state.field;
        const icon = button.querySelector('i');

        button.classList.toggle('is-active', isActive);

        if (!icon) return;

        icon.className = isActive
            ? `fa-solid ${state.direction === 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`
            : 'fa-solid fa-sort';
    });
}

function initVersionInlineEditing() {
    if (!canEditVersions()) return;

    document.querySelectorAll('.version-editable').forEach((element) => {
        if (element.dataset.inlineReady === 'true') return;

        element.dataset.inlineReady = 'true';
        element.title = 'Click to edit';

        element.addEventListener('click', () => openVersionInlineEditor(element));
        element.addEventListener('keydown', (event) => {
            if (event.key === 'Enter' || event.key === ' ') {
                event.preventDefault();
                openVersionInlineEditor(element);
            }
        });
    });
}

function initProjectInlineEditing() {
    if (!canEditProjects()) return;

    document.querySelectorAll('.project-editable').forEach((element) => {
        if (element.dataset.inlineReady === 'true') return;

        element.dataset.inlineReady = 'true';
        element.title = 'Click to edit';

        element.addEventListener('click', () => openProjectInlineEditor(element));
        element.addEventListener('keydown', (event) => {
            if (event.key === 'Enter' || event.key === ' ') {
                event.preventDefault();
                openProjectInlineEditor(element);
            }
        });
    });
}

async function openProjectInlineEditor(element) {
    if (!canEditProjects() || !currentProject || element.querySelector('.version-inline-form')) return;

    const field = element.dataset.projectField;
    const currentValue = element.dataset.currentValue ?? '';
    const originalHtml = element.innerHTML;
    const originalClasses = Array.from(element.classList);

    if (!['name', 'owner'].includes(field)) return;

    element.classList.remove('is-empty');
    element.innerHTML = '';

    const form = document.createElement('form');
    form.className = 'version-inline-form';

    let input;

    if (field === 'owner') {
        input = document.createElement('select');
        input.className = 'form-select version-inline-input';
        populateSelect(input, await getUsers(), {
            includeEmpty: false,
            selectedValue: currentValue
        });
    } else {
        input = document.createElement('input');
        input.className = 'form-control version-inline-input';
        input.type = 'text';
        input.required = true;
        input.maxLength = 128;
        input.value = currentValue;
    }

    form.appendChild(input);

    const actions = document.createElement('div');
    actions.className = 'version-inline-actions';
    actions.innerHTML = `
        <button class="btn btn-sm btn-primary" type="submit">
            <i class="fa-solid fa-check"></i>
        </button>
        <button class="btn btn-sm btn-outline-secondary" type="button" data-project-inline-cancel>
            <i class="fa-solid fa-xmark"></i>
        </button>
    `;
    form.appendChild(actions);

    element.appendChild(form);
    input.focus();
    if (input.select && input.tagName !== 'SELECT') {
        input.select();
    }

    let closed = false;
    const restore = () => {
        if (closed) return;

        closed = true;
        element.innerHTML = originalHtml;
        element.className = originalClasses.join(' ');
    };

    const save = async () => {
        const nextValue = field === 'name' ? input.value.trim() : input.value;

        if (nextValue === currentValue) {
            restore();
            return;
        }

        const result = await apiPost('/api/project.php', {
            api: 'Edit',
            project_id: currentProject.id
        }, {
            [field]: nextValue
        });

        if (!result.success) {
            alert(result.error || 'Could not update project.');
            restore();
            return;
        }

        taskLookupCache.projectsById.delete(currentProject.id);
        closed = true;
        await loadProject(currentProject.id, false);
        projectTreePromise = loadProjectTree();
    };

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        await save();
    });

    if (input.tagName === 'SELECT') {
        input.addEventListener('change', save);
    }

    form.querySelector('[data-project-inline-cancel]')?.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        restore();
    });

    input.addEventListener('keydown', async (event) => {
        if (event.key === 'Escape') {
            event.preventDefault();
            restore();
        }
    });
}

async function openVersionInlineEditor(element) {
    if (!canEditVersions() || !currentVersion || element.querySelector('.version-inline-form')) return;

    const field = element.dataset.versionField;
    const currentValue = element.dataset.currentValue ?? '';
    const originalHtml = element.innerHTML;
    const originalClasses = Array.from(element.classList);

    element.classList.remove('is-empty');
    element.innerHTML = '';

    const form = document.createElement('form');
    form.className = 'version-inline-form';

    const input = createVersionInlineInput(field, currentValue);
    form.appendChild(input);

    const actions = document.createElement('div');
    actions.className = 'version-inline-actions';
    actions.innerHTML = `
        <button class="btn btn-sm btn-primary" type="submit">
            <i class="fa-solid fa-check"></i>
        </button>
        <button class="btn btn-sm btn-outline-secondary" type="button" data-version-inline-cancel>
            <i class="fa-solid fa-xmark"></i>
        </button>
    `;
    form.appendChild(actions);

    element.appendChild(form);
    input.focus();

    if (input.select && input.type !== 'date') {
        input.select();
    }

    let editorClosed = false;

    const restore = () => {
        if (editorClosed) return;

        editorClosed = true;
        element.innerHTML = originalHtml;
        element.className = originalClasses.join(' ');
    };

    let saving = false;

    const save = async () => {
        if (saving) return;

        const nextValue = input.value;

        if (String(nextValue) === String(currentValue)) {
            restore();
            return;
        }

        saving = true;
        element.classList.add('is-saving');

        const result = await apiPost('/api/version.php', {
            api: 'Edit',
            version_id: currentVersion.id
        }, {
            [field]: nextValue
        });

        if (!result.success) {
            alert(result.error || 'Could not update version.');
            saving = false;
            restore();
            return;
        }

        editorClosed = true;
        element.classList.remove('is-saving');
        currentVersion = {
            ...currentVersion,
            [field]: nextValue === '' ? null : nextValue
        };

        taskLookupCache.versionsByProject.delete(currentVersion.project);
        await renderVersion(currentVersion);
        projectTreePromise = loadProjectTree();
        await projectTreePromise;
    };

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        await save();
    });

    form.querySelector('[data-version-inline-cancel]')?.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        restore();
    });

    input.addEventListener('keydown', async (event) => {
        if (event.key === 'Escape') {
            event.preventDefault();
            restore();
        }

        if (event.key === 'Enter' && input.tagName === 'TEXTAREA' && (event.ctrlKey || event.metaKey)) {
            event.preventDefault();
            await save();
        }
    });
}

function createVersionInlineInput(field, currentValue) {
    if (field === 'description') {
        const textarea = document.createElement('textarea');
        textarea.className = 'form-control version-inline-input';
        textarea.rows = 5;
        textarea.value = currentValue;
        return textarea;
    }

    const input = document.createElement('input');
    input.className = 'form-control version-inline-input';
    input.type = field === 'due_date' || field === 'released_date' ? 'date' : 'text';
    input.value = currentValue;
    input.required = field === 'name';
    return input;
}

async function getUserInfo(userId) {
    if (!userId || userId === '-') {
        return null;
    }

    const result = await apiGet('/api/user.php', {
        api: 'UserInfo',
        user_id: userId
    });

    return result.success ? result.user : null;
}

function renderUser(user) {
    if (!user) {
        return '-';
    }

    const avatar = user.picture
        ? `<img src="${user.picture}" alt="" class="user-avatar">`
        : `
            <span class="user-avatar user-avatar-fallback">
                <i class="fa-solid fa-user"></i>
            </span>
        `;

    return `
        <span class="meta-user">
            ${avatar}
            <span>${escapeHtml(user.name)}</span>
        </span>
    `;
}

function getCurrentUserId() {
    return document.querySelector('.kiln-app')?.dataset.currentUserId ?? '';
}

function updateAccountProfile(user) {
    setText('accountName', user.name ?? '');
    setText('accountEmail', user.email ?? '');

    const accountButton = document.querySelector('.kiln-account-btn');
    const accountName = document.getElementById('accountName');

    if (!accountButton || !accountName) return;

    document.getElementById('accountAvatarImage')?.remove();
    document.getElementById('accountAvatarFallback')?.remove();

    const avatar = document.createElement(user.picture ? 'img' : 'span');
    avatar.id = user.picture ? 'accountAvatarImage' : 'accountAvatarFallback';
    avatar.className = user.picture ? 'user-avatar' : 'user-avatar user-avatar-fallback';

    if (user.picture) {
        avatar.src = user.picture;
        avatar.alt = '';
    } else {
        avatar.innerHTML = '<i class="fa-solid fa-user"></i>';
    }

    accountButton.insertBefore(avatar, accountName);
}

function applyTheme(theme) {
    const stylesheet = document.getElementById('themeStylesheet');

    if (!stylesheet || !themeStyles[theme]) return;

    stylesheet.href = themeStyles[theme];
    stylesheet.dataset.theme = theme;
}

async function loadWorkflowEditor() {
    const result = await apiGet('/api/workflow.php', {
        api: 'WorkflowData',
        _: Date.now()
    });

    if (!result.success) {
        document.getElementById('workflowGraph').innerHTML = '<div class="alert alert-danger m-3">Could not load workflow data.</div>';
        return;
    }

    workflowData = result.workflow;
    if (!selectedWorkflowTaskType && workflowData.task_types.length) {
        selectedWorkflowTaskType = String(workflowData.task_types[0].id);
    }

    if (selectedWorkflowTaskType && !workflowData.task_types.some((type) => String(type.id) === String(selectedWorkflowTaskType))) {
        selectedWorkflowTaskType = workflowData.task_types.length ? String(workflowData.task_types[0].id) : null;
    }

    renderWorkflowEditor();
}

function renderWorkflowEditor() {
    if (!workflowData) return;

    populateWorkflowSelects();
    renderWorkflowLists();
    renderWorkflowGraph();
}

function populateWorkflowSelects() {
    const stateOptions = workflowData.states.map((state) => ({
        id: state.id,
        name: state.name
    }));

    populateSelect(document.getElementById('workflowTransitionFrom'), stateOptions, {
        placeholder: 'From state'
    });
    populateSelect(document.getElementById('workflowTransitionTo'), stateOptions, {
        placeholder: 'To state'
    });
    populateSelect(document.getElementById('workflowAssignmentState'), stateOptions, {
        placeholder: 'State'
    });
    populateSelect(document.getElementById('workflowTaskTypeSelect'), workflowData.task_types, {
        placeholder: 'Task type',
        selectedValue: selectedWorkflowTaskType ?? ''
    });

    const assignedStates = getSelectedWorkflowAssignments()
        .map((assignment) => workflowData.states.find((state) => Number(state.id) === Number(assignment.state)))
        .filter(Boolean);
    const selectedType = getSelectedWorkflowTaskType();

    populateSelect(document.getElementById('workflowDefaultStateSelect'), assignedStates, {
        placeholder: 'No default state',
        selectedValue: selectedType?.default_state ?? ''
    });
}

function renderWorkflowLists() {
    const statesById = new Map(workflowData.states.map((state) => [Number(state.id), state]));
    const selectedAssignments = getSelectedWorkflowAssignments();

    const stateList = document.getElementById('workflowStateList');
    stateList.innerHTML = workflowData.states.map((state) => `
        <div class="workflow-list-item">
            <div class="workflow-list-main">
                <span class="workflow-color-dot" style="background: ${escapeHtml(state.color)}"></span>
                <span>${escapeHtml(state.name)}</span>
            </div>
            <button class="btn btn-sm btn-outline-danger" type="button" data-workflow-delete-state="${escapeHtml(state.id)}">
                <i class="fa-solid fa-trash-can"></i>
            </button>
        </div>
    `).join('') || '<div class="text-secondary">No states yet.</div>';

    const transitionList = document.getElementById('workflowTransitionList');
    transitionList.innerHTML = workflowData.transitions.map((transition) => `
        <div class="workflow-list-item">
            <div class="workflow-list-main">
                <span>${escapeHtml(statesById.get(Number(transition.from_id))?.name ?? transition.from_id)}</span>
                <i class="fa-solid fa-arrow-right"></i>
                <span>${escapeHtml(statesById.get(Number(transition.to_id))?.name ?? transition.to_id)}</span>
                <span class="text-secondary">${escapeHtml(transition.action_name)}</span>
            </div>
            <button class="btn btn-sm btn-outline-danger" type="button" data-workflow-delete-transition="${escapeHtml(transition.id)}">
                <i class="fa-solid fa-trash-can"></i>
            </button>
        </div>
    `).join('') || '<div class="text-secondary">No transitions yet.</div>';

    const assignmentList = document.getElementById('workflowAssignmentList');
    assignmentList.innerHTML = selectedAssignments.map((assignment) => `
        <div class="workflow-list-item">
            <div class="workflow-list-main">
                <span>${escapeHtml(statesById.get(Number(assignment.state))?.name ?? assignment.state)}</span>
            </div>
            <button class="btn btn-sm btn-outline-danger" type="button" data-workflow-remove-assignment="${escapeHtml(assignment.id)}">
                <i class="fa-solid fa-trash-can"></i>
            </button>
        </div>
    `).join('') || '<div class="text-secondary">No assigned states yet.</div>';
}

function getSelectedWorkflowTaskType() {
    return workflowData?.task_types.find((type) => String(type.id) === String(selectedWorkflowTaskType)) ?? null;
}

function getSelectedWorkflowAssignments() {
    if (!selectedWorkflowTaskType) return [];

    return workflowData.assignments.filter((assignment) => String(assignment.task_type) === String(selectedWorkflowTaskType));
}

function renderWorkflowGraph() {
    const graph = document.getElementById('workflowGraph');
    const empty = document.getElementById('workflowGraphEmpty');

    if (!graph || !empty) return;

    const assignedStateIds = new Set(getSelectedWorkflowAssignments().map((assignment) => Number(assignment.state)));
    const visibleStates = workflowData.states.filter((state) => assignedStateIds.has(Number(state.id)));
    const visibleTransitions = workflowData.transitions.filter((transition) => (
        assignedStateIds.has(Number(transition.from_id))
        && assignedStateIds.has(Number(transition.to_id))
    ));

    if (!visibleStates.length) {
        if (workflowGraph) {
            workflowGraph.destroy();
            workflowGraph = null;
        }

        graph.innerHTML = '';
        empty.hidden = false;
        empty.textContent = selectedWorkflowTaskType
            ? 'Assign states to this task type to render its workflow graph.'
            : 'Select a task type to render its workflow graph.';
        return;
    }

    empty.hidden = true;

    if (typeof cytoscape !== 'function') {
        graph.innerHTML = '<div class="alert alert-warning m-3">Cytoscape.js could not be loaded.</div>';
        return;
    }

    const elements = [
        ...visibleStates.map((state) => ({
            data: {
                id: `state-${state.id}`,
                label: state.name,
                color: state.color
            }
        })),
        ...visibleTransitions.map((transition) => ({
            data: {
                id: `transition-${transition.id}`,
                source: `state-${transition.from_id}`,
                target: `state-${transition.to_id}`,
                label: transition.action_name
            }
        }))
    ];
    const styles = getComputedStyle(document.documentElement);
    const borderColor = styles.getPropertyValue('--bs-border-color').trim() || '#3a3f46';
    const bodyColor = styles.getPropertyValue('--bs-body-color').trim() || '#f3f4f6';
    const secondaryColor = styles.getPropertyValue('--bs-secondary-color').trim() || '#aeb7c4';

    if (workflowGraph) {
        workflowGraph.destroy();
    }

    workflowGraph = cytoscape({
        container: graph,
        elements,
        style: [
            {
                selector: 'node',
                style: {
                    'background-color': 'data(color)',
                    'border-color': borderColor,
                    'border-width': 1,
                    'color': bodyColor,
                    'font-size': 13,
                    'font-weight': 700,
                    'label': 'data(label)',
                    'text-valign': 'center',
                    'text-halign': 'center',
                    'text-wrap': 'wrap',
                    'text-max-width': 90,
                    'width': 86,
                    'height': 42,
                    'shape': 'round-rectangle'
                }
            },
            {
                selector: 'edge',
                style: {
                    'curve-style': 'bezier',
                    'target-arrow-shape': 'triangle',
                    'line-color': secondaryColor,
                    'target-arrow-color': secondaryColor,
                    'color': secondaryColor,
                    'font-size': 11,
                    'label': 'data(label)',
                    'text-rotation': 'autorotate',
                    'text-margin-y': -8,
                    'width': 2
                }
            }
        ],
        layout: {
            name: 'breadthfirst',
            directed: true,
            padding: 36,
            spacingFactor: 1.25
        }
    });
}

async function saveWorkflowAction(api, body = {}, params = {}) {
    const result = await apiPost('/api/workflow.php', {
        api,
        ...params
    }, body);

    if (!result.success) {
        alert(result.error || 'Could not save workflow change.');
        return false;
    }

    await loadWorkflowEditor();
    return true;
}

async function loadAdminOptions() {
    let result = null;

    try {
        result = await apiGet('/api/admin_options.php', {
            api: 'OptionsData',
            _: Date.now()
        });
    } catch (error) {
        result = {
            success: false,
            error: 'Could not load admin options.'
        };
    }

    if (!result.success) {
        const typeEditor = document.getElementById('adminSelectedTypeEditor');
        const priorityList = document.getElementById('adminPriorityList');
        const userRightsList = document.getElementById('adminUserRightsList');

        if (typeEditor) {
            typeEditor.innerHTML = '<div class="text-danger">Could not load task types.</div>';
        }

        if (priorityList) {
            priorityList.innerHTML = '<div class="text-danger">Could not load priorities.</div>';
        }

        if (userRightsList) {
            userRightsList.innerHTML = '<div class="text-danger">Could not load users.</div>';
        }

        return;
    }

    adminOptionsData = {
        types: result.options?.types ?? [],
        priorities: result.options?.priorities ?? [],
        custom_fields: result.options?.custom_fields ?? [],
        users: result.options?.users ?? [],
        rights: result.options?.rights ?? [],
        user_rights: result.options?.user_rights ?? [],
        projects: result.options?.projects ?? [],
        user_access: result.options?.user_access ?? []
    };
    if (!selectedAdminTaskType && adminOptionsData.types.length) {
        selectedAdminTaskType = String(adminOptionsData.types[0].id);
    }

    if (selectedAdminTaskType && !adminOptionsData.types.some((type) => String(type.id) === String(selectedAdminTaskType))) {
        selectedAdminTaskType = adminOptionsData.types.length ? String(adminOptionsData.types[0].id) : null;
    }

    if (!selectedAdminUser && adminOptionsData.users.length) {
        selectedAdminUser = String(adminOptionsData.users[0].id);
    }

    if (selectedAdminUser && !adminOptionsData.users.some((user) => String(user.id) === String(selectedAdminUser))) {
        selectedAdminUser = adminOptionsData.users.length ? String(adminOptionsData.users[0].id) : null;
    }

    renderAdminOptions();
}

function renderAdminOptions() {
    if (!adminOptionsData) return;

    const types = adminOptionsData.types ?? [];
    const priorities = adminOptionsData.priorities ?? [];

    populateSelect(document.getElementById('adminTaskTypeSelect'), types, {
        placeholder: 'Select task type',
        selectedValue: selectedAdminTaskType ?? ''
    });
    renderSelectedAdminTaskType();
    renderAdminCustomFields();
    renderAdminOptionList('priority', priorities, document.getElementById('adminPriorityList'));
    renderAdminUsers();
}

function getSelectedAdminTaskType() {
    return adminOptionsData?.types.find((type) => String(type.id) === String(selectedAdminTaskType)) ?? null;
}

function renderSelectedAdminTaskType() {
    const container = document.getElementById('adminSelectedTypeEditor');

    if (!container) return;

    const type = getSelectedAdminTaskType();

    if (!type) {
        container.innerHTML = '<div class="text-secondary">Create or select a task type first.</div>';
        return;
    }

    container.innerHTML = `
        <form class="admin-option-item admin-selected-option" data-admin-option-edit="type" data-option-id="${escapeHtml(type.id)}">
            <div class="admin-option-icon">
                ${type.icon
                    ? `<img src="${escapeHtml(type.icon)}" alt="">`
                    : '<i class="fa-solid fa-tag"></i>'}
            </div>
            <input class="form-control form-control-sm" name="name" value="${escapeHtml(type.name)}" required maxlength="20">
            <input class="form-control form-control-sm" type="file" name="logo" accept=".svg,image/svg+xml" aria-label="Logo">
            <label class="admin-option-remove-logo">
                <input type="checkbox" name="remove_logo" value="1">
                <span>Remove logo</span>
            </label>
            <div class="admin-option-actions">
                <button class="btn btn-sm btn-primary" type="submit" title="Save">
                    <i class="fa-solid fa-check"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" type="button" data-admin-option-delete="type" data-option-id="${escapeHtml(type.id)}" title="Delete">
                    <i class="fa-solid fa-trash-can"></i>
                </button>
            </div>
        </form>
    `;
}

function renderAdminCustomFields() {
    const container = document.getElementById('adminCustomFieldList');

    if (!container) return;

    if (!selectedAdminTaskType) {
        container.innerHTML = '<div class="text-secondary">Select a task type first.</div>';
        return;
    }

    const fields = (adminOptionsData.custom_fields ?? [])
        .filter((field) => String(field.task_type) === String(selectedAdminTaskType));

    container.innerHTML = fields.map((field) => `
        <form class="admin-custom-field-item" data-admin-custom-field-edit="${escapeHtml(field.id)}">
            <input class="form-control form-control-sm" name="name" value="${escapeHtml(field.name)}" required maxlength="128">
            <select class="form-select form-select-sm" name="type" required>
                ${customFieldTypes.map((type) => `
                    <option value="${escapeHtml(type.id)}" ${String(type.id) === String(field.type) ? 'selected' : ''}>${escapeHtml(type.name)}</option>
                `).join('')}
            </select>
            <input class="form-control form-control-sm" name="value" value="${escapeHtml(field.raw_value ?? '')}">
            <div class="admin-option-actions">
                <button class="btn btn-sm btn-primary" type="submit" title="Save">
                    <i class="fa-solid fa-check"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" type="button" data-admin-custom-field-delete="${escapeHtml(field.id)}" title="Delete">
                    <i class="fa-solid fa-trash-can"></i>
                </button>
            </div>
        </form>
    `).join('') || '<div class="text-secondary">No custom fields yet.</div>';
}

function renderAdminOptionList(kind, options, container) {
    if (!container) return;

    container.innerHTML = options.map((option) => `
        <form class="admin-option-item" data-admin-option-edit="${escapeHtml(kind)}" data-option-id="${escapeHtml(option.id)}">
            <div class="admin-option-icon">
                ${option.icon
                    ? `<img src="${escapeHtml(option.icon)}" alt="">`
                    : '<i class="fa-solid fa-tag"></i>'}
            </div>
            <input class="form-control form-control-sm" name="name" value="${escapeHtml(option.name)}" required maxlength="128">
            <input class="form-control form-control-sm" type="file" name="logo" accept=".svg,image/svg+xml" aria-label="Logo">
            <label class="admin-option-remove-logo">
                <input type="checkbox" name="remove_logo" value="1">
                <span>Remove logo</span>
            </label>
            <div class="admin-option-actions">
                <button class="btn btn-sm btn-primary" type="submit" title="Save">
                    <i class="fa-solid fa-check"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" type="button" data-admin-option-delete="${escapeHtml(kind)}" data-option-id="${escapeHtml(option.id)}" title="Delete">
                    <i class="fa-solid fa-trash-can"></i>
                </button>
            </div>
        </form>
    `).join('') || '<div class="text-secondary">No entries yet.</div>';
}

function getSelectedAdminUser() {
    return adminOptionsData?.users.find((user) => String(user.id) === String(selectedAdminUser)) ?? null;
}

function userHasAdminRight(userId, rightId) {
    return (adminOptionsData?.user_rights ?? []).some((entry) => (
        String(entry.user_id) === String(userId)
        && String(entry.right_id) === String(rightId)
    ));
}

function getAdminUserAccess(userId) {
    return (adminOptionsData?.user_access ?? []).filter((entry) => (
        String(entry.user_id) === String(userId)
    ));
}

function getAdminProject(projectId) {
    return (adminOptionsData?.projects ?? []).find((project) => (
        String(project.id) === String(projectId)
    )) ?? null;
}

function renderAdminUsers() {
    const select = document.getElementById('adminUserSelect');
    const container = document.getElementById('adminUserRightsList');

    if (!select || !container) return;

    const users = adminOptionsData.users ?? [];
    const rights = adminOptionsData.rights ?? [];
    const projects = adminOptionsData.projects ?? [];
    const selectedUser = getSelectedAdminUser();

    populateSelect(select, users, {
        placeholder: 'Select user',
        selectedValue: selectedAdminUser ?? ''
    });

    if (!selectedUser) {
        container.innerHTML = '<div class="text-secondary">Select a user first.</div>';
        return;
    }

    if (!rights.length) {
        container.innerHTML = '<div class="text-secondary">No rights configured yet.</div>';
        return;
    }

    const explicitAccess = getAdminUserAccess(selectedUser.id);
    const explicitProjectIds = new Set(explicitAccess.map((entry) => String(entry.project_id)));
    const ownerProjects = projects.filter((project) => String(project.owner) === String(selectedUser.id));
    const ownerProjectIds = new Set(ownerProjects.map((project) => String(project.id)));
    const addableProjects = projects.filter((project) => (
        !explicitProjectIds.has(String(project.id))
        && !ownerProjectIds.has(String(project.id))
    ));

    container.innerHTML = `
        <div class="admin-user-summary">
            ${renderUser(selectedUser)}
        </div>

        <div class="admin-user-section">
            <h4>Rights</h4>
            <div class="admin-right-list">
                ${rights.map((right) => {
                    const checked = userHasAdminRight(selectedUser.id, right.id);
                    const isSelfAdminRight = appFlag('isAdmin')
                        && String(selectedUser.id) === String(getCurrentUserId())
                        && String(right.name) === 'Admin';

                    return `
                        <label class="admin-right-item">
                            <input
                                type="checkbox"
                                data-admin-user-right
                                data-user-id="${escapeHtml(selectedUser.id)}"
                                data-right-id="${escapeHtml(right.id)}"
                                ${checked ? 'checked' : ''}
                                ${isSelfAdminRight ? 'disabled' : ''}
                            >
                            <span>${escapeHtml(right.name)}</span>
                        </label>
                    `;
                }).join('')}
            </div>
        </div>

        <div class="admin-user-section">
            <h4>Project Access</h4>
            <form class="admin-project-access-form" id="adminProjectAccessForm">
                <select class="form-select form-select-sm" name="project_id" ${addableProjects.length ? '' : 'disabled'}>
                    ${addableProjects.map((project) => `
                        <option value="${escapeHtml(project.id)}">${escapeHtml(project.name)} (${escapeHtml(project.id)})</option>
                    `).join('')}
                </select>
                <button class="btn btn-sm btn-primary" type="submit" ${addableProjects.length ? '' : 'disabled'}>
                    <i class="fa-solid fa-plus me-1"></i>
                    Add Access
                </button>
            </form>

            <div class="admin-project-access-list">
                ${explicitAccess.map((access) => {
                    const project = getAdminProject(access.project_id);

                    return `
                        <div class="admin-project-access-item">
                            <span>${escapeHtml(project?.name ?? access.project_id)} <small>${escapeHtml(access.project_id)}</small></span>
                            <button class="btn btn-sm btn-outline-danger" type="button" data-admin-project-access-delete="${escapeHtml(access.id)}" title="Remove access">
                                <i class="fa-solid fa-trash-can"></i>
                            </button>
                        </div>
                    `;
                }).join('') || '<div class="text-secondary">No explicit project access yet.</div>'}
            </div>

            ${ownerProjects.length ? `
                <div class="admin-project-owner-list">
                    <strong>Owner access</strong>
                    ${ownerProjects.map((project) => `
                        <span>${escapeHtml(project.name)} <small>${escapeHtml(project.id)}</small></span>
                    `).join('')}
                </div>
            ` : ''}
        </div>
    `;
}

async function setAdminUserRight(userId, rightId, enabled) {
    const result = await apiPost('/api/admin_options.php', {
        api: 'SetUserRight'
    }, {
        user_id: userId,
        right_id: rightId,
        enabled: enabled ? 1 : 0
    });

    if (!result.success) {
        alert(result.error || 'Could not update user right.');
        return false;
    }

    await loadAdminOptions();
    return true;
}

async function grantAdminProjectAccess(userId, projectId) {
    const result = await apiPost('/api/admin_options.php', {
        api: 'GrantProjectAccess'
    }, {
        user_id: userId,
        project_id: projectId
    });

    if (!result.success) {
        alert(result.error || 'Could not add project access.');
        return false;
    }

    await loadAdminOptions();
    return true;
}

async function revokeAdminProjectAccess(accessId) {
    const result = await apiPost('/api/admin_options.php', {
        api: 'RevokeProjectAccess',
        access_id: accessId
    });

    if (!result.success) {
        alert(result.error || 'Could not remove project access.');
        return false;
    }

    await loadAdminOptions();
    return true;
}

async function saveAdminOptionForm(kind, form, optionId = null) {
    const formData = new FormData(form);
    formData.set('kind', kind);

    if (optionId) {
        formData.set('id', optionId);
    }

    const result = await apiPostForm('/api/admin_options.php', {
        api: optionId ? 'UpdateOption' : 'CreateOption'
    }, formData);

    if (!result.success) {
        alert(result.error || 'Could not save option.');
        return false;
    }

    if (kind === 'type' && !optionId && result.option_id) {
        selectedAdminTaskType = String(result.option_id);
    }

    taskLookupCache.types = null;
    taskLookupCache.priorities = null;
    await populateTaskOptionSelects();
    await loadAdminOptions();

    return true;
}

async function deleteAdminOption(kind, optionId) {
    const result = await apiPost('/api/admin_options.php', {
        api: 'DeleteOption',
        kind,
        id: optionId
    });

    if (!result.success) {
        alert(result.error || 'Could not delete option.');
        return false;
    }

    if (kind === 'type' && String(selectedAdminTaskType) === String(optionId)) {
        selectedAdminTaskType = null;
    }

    taskLookupCache.types = null;
    taskLookupCache.priorities = null;
    await populateTaskOptionSelects();
    await loadAdminOptions();

    return true;
}

async function saveAdminCustomFieldForm(form, fieldId = null) {
    if (!selectedAdminTaskType) {
        alert('Select a task type first.');
        return false;
    }

    const data = Object.fromEntries(new FormData(form));
    data.task_type = selectedAdminTaskType;

    if (fieldId) {
        data.id = fieldId;
    }

    const result = await apiPost('/api/admin_options.php', {
        api: fieldId ? 'UpdateCustomField' : 'CreateCustomField'
    }, data);

    if (!result.success) {
        alert(result.error || 'Could not save custom field.');
        return false;
    }

    await loadAdminOptions();
    return true;
}

async function deleteAdminCustomField(fieldId) {
    const result = await apiPost('/api/admin_options.php', {
        api: 'DeleteCustomField',
        field_id: fieldId
    });

    if (!result.success) {
        alert(result.error || 'Could not delete custom field.');
        return false;
    }

    await loadAdminOptions();
    return true;
}

function escapeHtml(value) {
    return String(value)
        .replaceAll('&', '&amp;')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#039;');
}

function loadRouteFromUrl() {
    const params = new URLSearchParams(window.location.search);
    const taskId = params.get('task');
    const versionId = params.get('version');
    const projectId = params.get('project');
    const projectTasks = params.get('tasks');
    const profile = params.get('profile');
    const adminSection = params.get('admin');

    closePopups();

    if (adminSection) {
        loadAdmin(adminSection, false);
        return;
    }

    if (profile) {
        loadProfile(false);
        return;
    }

    if (taskId) {
        loadTask(taskId, false);
        return;
    }

    if (versionId) {
        loadVersion(versionId, false);
        return;
    }

    if (projectId) {
        if (projectTasks) {
            loadProjectTasks(projectId, 1, false);
        } else {
            loadProject(projectId, false);
        }
        return;
    }

    showView('dashboardView');
}

function loadAdmin(section = 'workflows', pushHistory = true) {
    if (document.querySelector('.kiln-app')?.dataset.isAdmin !== '1') {
        showView('dashboardView');
        return;
    }

    const view = showView('adminView');

    if (!view) return;

    if (!['workflows', 'options', 'users'].includes(section)) {
        section = 'workflows';
    }

    document.querySelectorAll('[data-admin-section]').forEach((button) => {
        button.classList.toggle('is-active', button.dataset.adminSection === section);
    });

    document.querySelectorAll('.admin-panel').forEach((panel) => {
        panel.hidden = true;
    });

    const panelBySection = {
        workflows: 'adminWorkflowsPanel',
        options: 'adminOptionsPanel',
        users: 'adminUsersPanel'
    };
    const panel = document.getElementById(panelBySection[section]);
    if (panel) panel.hidden = false;

    if (pushHistory) {
        const url = new URL(window.location.href);
        url.searchParams.set('page', 'home');
        url.searchParams.set('admin', section);
        url.searchParams.delete('task');
        url.searchParams.delete('project');
        url.searchParams.delete('tasks');
        url.searchParams.delete('version');
        url.searchParams.delete('profile');
        window.history.pushState({}, '', url);
    }

    if (section === 'workflows') {
        loadWorkflowEditor();
    } else if (section === 'options' || section === 'users') {
        loadAdminOptions();
    }
}

document.addEventListener('DOMContentLoaded', () => {
    projectTreePromise = loadProjectTree();
    populateTaskOptionSelects();
    applyPermissionVisibility();
    initTaskInlineEditing();
    initVersionInlineEditing();
    initProjectInlineEditing();

    loadRouteFromUrl();

    window.addEventListener('popstate', () => {
        loadRouteFromUrl();
    });

    document.querySelectorAll('[data-view="dashboard"]').forEach((button) => {
        button.addEventListener('click', () => {
            showView('dashboardView');

            const url = new URL(window.location.href);
            url.searchParams.set('page', 'home');
            url.searchParams.delete('task');
            url.searchParams.delete('project');
            url.searchParams.delete('tasks');
            url.searchParams.delete('version');
            url.searchParams.delete('profile');
            url.searchParams.delete('admin');
            window.history.pushState({}, '', url);
        });
    });

    document.getElementById('adminMenuButton')?.addEventListener('click', () => {
        loadAdmin('workflows');
    });

    document.querySelectorAll('[data-admin-section]').forEach((button) => {
        button.addEventListener('click', () => {
            loadAdmin(button.dataset.adminSection);
        });
    });

    document.getElementById('workflowRefreshButton')?.addEventListener('click', () => {
        loadWorkflowEditor();
    });

    document.getElementById('workflowTaskTypeSelect')?.addEventListener('change', (event) => {
        selectedWorkflowTaskType = event.target.value || null;
        renderWorkflowEditor();
    });

    document.getElementById('workflowDefaultStateSelect')?.addEventListener('change', (event) => {
        if (!selectedWorkflowTaskType) return;

        saveWorkflowAction('SetDefaultState', {
            task_type: selectedWorkflowTaskType,
            state: event.target.value
        });
    });

    document.getElementById('workflowStateForm')?.addEventListener('submit', async (event) => {
        event.preventDefault();

        const form = event.currentTarget;
        const data = Object.fromEntries(new FormData(form));
        const saved = await saveWorkflowAction('CreateState', data);

        if (saved) form.reset();
    });

    document.getElementById('workflowTransitionForm')?.addEventListener('submit', async (event) => {
        event.preventDefault();

        const form = event.currentTarget;
        const data = Object.fromEntries(new FormData(form));
        const saved = await saveWorkflowAction('CreateTransition', data);

        if (saved) form.reset();
    });

    document.getElementById('workflowAssignmentForm')?.addEventListener('submit', async (event) => {
        event.preventDefault();

        const form = event.currentTarget;
        const data = Object.fromEntries(new FormData(form));
        data.task_type = selectedWorkflowTaskType;

        if (!data.task_type) {
            alert('Select a task type first.');
            return;
        }

        const saved = await saveWorkflowAction('AssignState', data);

        if (saved) form.reset();
    });

    document.getElementById('adminWorkflowsPanel')?.addEventListener('click', (event) => {
        const stateButton = event.target.closest('[data-workflow-delete-state]');
        const transitionButton = event.target.closest('[data-workflow-delete-transition]');
        const assignmentButton = event.target.closest('[data-workflow-remove-assignment]');

        if (stateButton) {
            if (!window.confirm('Delete this state and related transitions/assignments?')) return;

            saveWorkflowAction('DeleteState', {}, {
                state_id: stateButton.dataset.workflowDeleteState
            });
        }

        if (transitionButton) {
            saveWorkflowAction('DeleteTransition', {}, {
                transition_id: transitionButton.dataset.workflowDeleteTransition
            });
        }

        if (assignmentButton) {
            saveWorkflowAction('RemoveAssignedState', {}, {
                assignment_id: assignmentButton.dataset.workflowRemoveAssignment
            });
        }
    });

    document.querySelectorAll('[data-admin-option-form]').forEach((form) => {
        form.addEventListener('submit', async (event) => {
            event.preventDefault();

            const saved = await saveAdminOptionForm(form.dataset.adminOptionForm, form);

            if (saved) form.reset();
        });
    });

    document.getElementById('adminTaskTypeSelect')?.addEventListener('change', (event) => {
        selectedAdminTaskType = event.target.value || null;
        renderAdminOptions();
    });

    document.getElementById('adminUserSelect')?.addEventListener('change', (event) => {
        selectedAdminUser = event.target.value || null;
        renderAdminOptions();
    });

    document.getElementById('adminCustomFieldForm')?.addEventListener('submit', async (event) => {
        event.preventDefault();

        const saved = await saveAdminCustomFieldForm(event.currentTarget);

        if (saved) event.currentTarget.reset();
    });

    document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => {
        const form = event.target.closest('[data-admin-option-edit]');

        if (!form) return;

        event.preventDefault();
        await saveAdminOptionForm(form.dataset.adminOptionEdit, form, form.dataset.optionId);
    });

    document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => {
        const form = event.target.closest('[data-admin-custom-field-edit]');

        if (!form) return;

        event.preventDefault();
        await saveAdminCustomFieldForm(form, form.dataset.adminCustomFieldEdit);
    });

    document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => {
        const deleteButton = event.target.closest('[data-admin-option-delete]');

        if (!deleteButton) return;

        if (!window.confirm('Delete this option?')) return;

        deleteAdminOption(deleteButton.dataset.adminOptionDelete, deleteButton.dataset.optionId);
    });

    document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => {
        const deleteButton = event.target.closest('[data-admin-custom-field-delete]');

        if (!deleteButton) return;

        if (!window.confirm('Delete this custom field?')) return;

        deleteAdminCustomField(deleteButton.dataset.adminCustomFieldDelete);
    });

    document.getElementById('adminUsersPanel')?.addEventListener('change', async (event) => {
        const checkbox = event.target.closest('[data-admin-user-right]');

        if (!checkbox) return;

        checkbox.disabled = true;
        const saved = await setAdminUserRight(
            checkbox.dataset.userId,
            checkbox.dataset.rightId,
            checkbox.checked
        );

        if (!saved) {
            checkbox.checked = !checkbox.checked;
            checkbox.disabled = false;
        }
    });

    document.getElementById('adminUsersPanel')?.addEventListener('submit', async (event) => {
        const form = event.target.closest('#adminProjectAccessForm');

        if (!form) return;

        event.preventDefault();

        if (!selectedAdminUser) return;

        const data = Object.fromEntries(new FormData(form));
        if (!data.project_id) return;

        await grantAdminProjectAccess(selectedAdminUser, data.project_id);
    });

    document.getElementById('adminUsersPanel')?.addEventListener('click', async (event) => {
        const deleteButton = event.target.closest('[data-admin-project-access-delete]');

        if (!deleteButton) return;

        await revokeAdminProjectAccess(deleteButton.dataset.adminProjectAccessDelete);
    });

    document.getElementById('profileMenuButton')?.addEventListener('click', () => {
        loadProfile();
    });

    document.getElementById('taskEditButton')?.addEventListener('click', () => {
        if (!canEditTasks()) return;
        openTaskEditPopup();
    });

    document.getElementById('taskView')?.addEventListener('click', (event) => {
        const customField = event.target.closest('[data-custom-field-id]');

        if (customField) {
            if (!canEditTasks()) return;
            openCustomFieldInlineEditor(customField);
            return;
        }

        const statusButton = event.target.closest('#taskStatusButton');

        if (statusButton) {
            const menu = document.getElementById('taskStatusMenu');
            if (menu) menu.hidden = !menu.hidden;
            return;
        }

        const transitionButton = event.target.closest('[data-task-transition]');

        if (transitionButton) {
            transitionCurrentTask(transitionButton.dataset.taskTransition);
        }
    });

    document.getElementById('taskView')?.addEventListener('keydown', (event) => {
        if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(event.target.tagName)) return;

        const customField = event.target.closest('[data-custom-field-id]');

        if (!customField || !['Enter', ' '].includes(event.key)) return;

        event.preventDefault();
        if (!canEditTasks()) return;
        openCustomFieldInlineEditor(customField);
    });

    document.getElementById('taskCommentButton')?.addEventListener('click', () => {
        document.getElementById('taskCommentInput')?.focus();
    });

    document.getElementById('versionEditButton')?.addEventListener('click', () => {
        if (!canEditVersions()) return;
        openVersionEditPopup();
    });

    document.getElementById('projectEditButton')?.addEventListener('click', () => {
        if (!canEditProjects()) return;
        openProjectEditPopup();
    });

    document.querySelectorAll('[data-version-task-sort]').forEach((button) => {
        button.addEventListener('click', () => {
            const field = button.dataset.versionTaskSort;

            if (versionTaskSort.field === field) {
                versionTaskSort.direction = versionTaskSort.direction === 'asc' ? 'desc' : 'asc';
            } else {
                versionTaskSort.field = field;
                versionTaskSort.direction = 'asc';
            }

            if (currentVersion) {
                loadVersionTasks(currentVersion.id, 1);
            }
        });
    });

    document.querySelectorAll('[data-project-task-sort]').forEach((button) => {
        button.addEventListener('click', () => {
            const field = button.dataset.projectTaskSort;

            if (projectTaskSort.field === field) {
                projectTaskSort.direction = projectTaskSort.direction === 'asc' ? 'desc' : 'asc';
            } else {
                projectTaskSort.field = field;
                projectTaskSort.direction = 'asc';
            }

            if (currentProjectTaskProject) {
                loadProjectTasks(currentProjectTaskProject, 1);
            }
        });
    });

    document.addEventListener('click', (event) => {
        const pageButton = event.target.closest('[data-task-page]');

        if (!pageButton || pageButton.disabled) return;

        const page = Number(pageButton.dataset.page);

        if (!Number.isFinite(page) || page < 1) return;

        if (pageButton.dataset.taskPage === 'version' && currentVersion) {
            loadVersionTasks(currentVersion.id, page);
        }

        if (pageButton.dataset.taskPage === 'project' && currentProjectTaskProject) {
            loadProjectTasks(currentProjectTaskProject, page);
        }
    });

    const createProjectForm = document.getElementById('createProjectForm');

    if (createProjectForm) {
        createProjectForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const data = Object.fromEntries(new FormData(createProjectForm));
            const mode = createProjectForm.dataset.mode ?? 'create';
            const projectId = document.getElementById('projectFormId').value;

            const result = await apiPost('/api/project.php', {
                api: mode === 'edit' ? 'Edit' : 'Create',
                project_id: mode === 'edit' ? projectId : undefined
            }, data);

            if (result.success) {
                taskLookupCache.projectsById.delete(result.project_id);
                closePopups();
                createProjectForm.reset();
                projectTreePromise = loadProjectTree();

                if (mode === 'edit') {
                    await loadProject(projectId, false);
                }
            } else {
                alert(result.error || 'Could not create project.');
            }
        });
    }

    const createVersionForm = document.getElementById('createVersionForm');

    if (createVersionForm) {
        createVersionForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const data = Object.fromEntries(new FormData(createVersionForm));
            const mode = createVersionForm.dataset.mode ?? 'create';
            const versionId = data.version_id;
            const projectId = data.project_id;
            delete data.version_id;

            if (mode === 'edit') {
                delete data.project_id;
            }

            const result = await apiPost('/api/version.php', {
                api: mode === 'edit' ? 'Edit' : 'Create',
                version_id: mode === 'edit' ? versionId : undefined
            }, data);

            if (result.success) {
                taskLookupCache.versionsByProject.delete(projectId);
                closePopups();
                projectTreePromise = loadProjectTree();

                if (mode === 'edit') {
                    await loadVersion(versionId, false);
                }
            } else {
                alert(result.error || 'Could not save version.');
            }
        });
    }

    const profileForm = document.getElementById('profileForm');

    if (profileForm) {
        document.getElementById('profilePictureInput')?.addEventListener('change', (event) => {
            const file = event.target.files?.[0];

            if (!file) {
                renderProfileAvatar(currentProfileUser?.picture ?? null);
                return;
            }

            document.getElementById('profileRemovePicture').checked = false;

            const reader = new FileReader();
            reader.addEventListener('load', () => renderProfileAvatar(reader.result));
            reader.readAsDataURL(file);
        });

        document.getElementById('profileRemovePicture')?.addEventListener('change', (event) => {
            if (event.target.checked) {
                document.getElementById('profilePictureInput').value = '';
                renderProfileAvatar(null);
            } else {
                renderProfileAvatar(currentProfileUser?.picture ?? null);
            }
        });

        profileForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const password = document.getElementById('profilePassword').value;
            const passwordConfirm = document.getElementById('profilePasswordConfirm').value;
            const status = document.getElementById('profileStatus');
            const submitButton = document.getElementById('profileSubmitButton');

            if (password !== passwordConfirm) {
                status.textContent = 'Passwords do not match.';
                return;
            }

            const data = new FormData(profileForm);

            if (!password) {
                data.delete('password');
            }

            submitButton.disabled = true;
            status.textContent = 'Saving...';

            const result = await apiPostForm('/api/user.php', {
                api: 'Edit',
                user_id: getCurrentUserId()
            }, data);

            submitButton.disabled = false;

            if (!result.success) {
                status.textContent = result.error || 'Could not save profile.';
                return;
            }

            const userResult = await apiGet('/api/user.php', {
                api: 'UserInfo',
                user_id: getCurrentUserId(),
                _: Date.now()
            });

            if (userResult.success) {
                taskLookupCache.users = null;
                updateAccountProfile(userResult.user);
                applyTheme(userResult.user.settings?.theme ?? 'dark');
                renderProfile(userResult.user);
                status.textContent = 'Saved.';
            } else {
                status.textContent = 'Saved. Refresh to see all changes.';
            }
        });
    }

    const createTaskForm = document.getElementById('createTaskForm');

    if (createTaskForm) {
        document.getElementById('taskFormProjectId')?.addEventListener('change', (event) => {
            populateTaskRelationSelects(event.target.value.trim());
        });

        createTaskForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const data = Object.fromEntries(new FormData(createTaskForm));
            const mode = createTaskForm.dataset.mode ?? 'create';
            const taskId = data.task_id;
            delete data.task_id;

            if (mode === 'edit') {
                delete data.project_id;
            }

            const result = await apiPost('/api/task.php', {
                api: mode === 'edit' ? 'Edit' : 'Create',
                task_id: mode === 'edit' ? taskId : undefined
            }, data);

            if (result.success) {
                closePopups();
                projectTreePromise = loadProjectTree();
                loadTask(mode === 'edit' ? taskId : result.task_id, mode !== 'edit');
            } else {
                alert(result.error || 'Could not save task.');
            }
        });
    }

    const taskCommentForm = document.getElementById('taskCommentForm');

    if (taskCommentForm) {
        taskCommentForm.addEventListener('submit', async (event) => {
            event.preventDefault();

            const input = document.getElementById('taskCommentInput');
            const status = document.getElementById('taskCommentStatus');
            const comment = input.value.trim();

            if (!comment) {
                status.textContent = 'Comment cannot be empty.';
                return;
            }

            status.textContent = 'Saving...';

            try {
                await submitTaskComment(comment);
                input.value = '';
                status.textContent = 'Saved.';
            } catch (error) {
                status.textContent = error.message;
            }
        });
    }

    document.getElementById('taskCommentList')?.addEventListener('click', (event) => {
        const replyButton = event.target.closest('[data-comment-reply]');
        const editButton = event.target.closest('[data-comment-edit]');
        const deleteButton = event.target.closest('[data-comment-delete]');
        const cancelButton = event.target.closest('[data-comment-reply-cancel]');
        const editCancelButton = event.target.closest('[data-comment-edit-cancel]');

        if (replyButton) {
            const commentCard = replyButton.closest('.task-comment');
            const slot = commentCard?.querySelector('.task-comment-reply-slot');

            if (!slot || slot.querySelector('form')) return;

            slot.innerHTML = `
                <form class="task-comment-reply-form" data-comment-reply-form="${replyButton.dataset.commentReply}">
                    <textarea class="form-control" name="comment" rows="2" placeholder="Write a reply..."></textarea>
                    <div class="task-comment-reply-actions">
                        <button class="btn btn-sm btn-primary" type="submit">Reply</button>
                        <button class="btn btn-sm btn-outline-secondary" type="button" data-comment-reply-cancel>Cancel</button>
                    </div>
                </form>
            `;
            slot.querySelector('textarea')?.focus();
        }

        if (editButton) {
            const commentCard = editButton.closest('.task-comment');
            const slot = commentCard?.querySelector('.task-comment-edit-slot');

            if (!slot || slot.querySelector('form')) return;

            slot.innerHTML = `
                <form class="task-comment-edit-form" data-comment-edit-form="${editButton.dataset.commentEdit}">
                    <textarea class="form-control" name="comment" rows="3">${escapeHtml(commentCard.dataset.commentText ?? '')}</textarea>
                    <div class="task-comment-reply-actions">
                        <button class="btn btn-sm btn-primary" type="submit">Save</button>
                        <button class="btn btn-sm btn-outline-secondary" type="button" data-comment-edit-cancel>Cancel</button>
                    </div>
                </form>
            `;
            slot.querySelector('textarea')?.focus();
        }

        if (deleteButton) {
            if (!window.confirm('Delete this comment?')) return;

            deleteTaskComment(deleteButton.dataset.commentDelete).catch((error) => {
                alert(error.message);
            });
        }

        if (cancelButton) {
            cancelButton.closest('.task-comment-reply-slot').innerHTML = '';
        }

        if (editCancelButton) {
            editCancelButton.closest('.task-comment-edit-slot').innerHTML = '';
        }
    });

    document.getElementById('taskCommentList')?.addEventListener('submit', async (event) => {
        const form = event.target.closest('[data-comment-reply-form], [data-comment-edit-form]');

        if (!form) return;

        event.preventDefault();

        const textarea = form.querySelector('textarea');
        const button = form.querySelector('button[type="submit"]');
        const comment = textarea.value.trim();
        const responseTo = form.dataset.commentReplyForm;
        const editCommentId = form.dataset.commentEditForm;

        if (!comment) return;

        button.disabled = true;

        try {
            if (editCommentId) {
                await updateTaskComment(editCommentId, comment);
            } else {
                await submitTaskComment(comment, responseTo);
            }
        } catch (error) {
            alert(error.message);
            button.disabled = false;
        }
    });
});
