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');
        });
    });
}

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 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;
}

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