{% extends 'atlas/base.html' %}
{% load static %}

{% block content %}
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
        <div>
            <h2 class="mb-1">Web DICOM Upload</h2>
            <p class="text-muted mb-0">Select a folder, review grouped studies/series, run anonymisation checks, then upload.</p>
        </div>
        <a class="btn btn-outline-secondary" href="{% url 'atlas:user_uploads' %}">
            <i class="bi bi-hourglass-split me-1"></i>Uploads Awaiting Import
        </a>
    </div>

    <div class="alert alert-warning" role="alert">
        <strong>Important:</strong> this page does not anonymise files. It blocks uploads when anonymisation warnings are detected unless manually overridden.
    </div>

    <div class="card mb-3">
        <div class="card-body">
            <div class="row g-3 align-items-end">
                <div class="col-lg-8">
                    <label for="filepicker" class="form-label fw-semibold">Select folder containing DICOM files</label>
                    <input type="file" class="form-control" name="dicom_files" id="filepicker" webkitdirectory multiple>
                    <div class="form-text">Subfolders are scanned recursively. Non-DICOM files are ignored.</div>
                </div>
                <div class="col-lg-4">
                    <button type="button" id="uploadButton" class="btn btn-primary w-100" disabled>
                        <i class="bi bi-cloud-arrow-up me-1"></i>Upload Selected
                    </button>
                </div>
            </div>

            <details class="mt-3">
                <summary class="fw-semibold">Upload Settings</summary>
                <div class="row g-2 mt-1">
                    <div class="col-md-6 col-xl-3">
                        <div class="form-check form-switch">
                            <input class="form-check-input" type="checkbox" id="check-anonymisation-tags" checked>
                            <label class="form-check-label" for="check-anonymisation-tags">Check tags anonymisation</label>
                        </div>
                    </div>
                    <div class="col-md-6 col-xl-3">
                        <div class="form-check form-switch">
                            <input class="form-check-input" type="checkbox" id="check-anonymisation-ocr">
                            <label class="form-check-label" for="check-anonymisation-ocr">Check burned-in text (OCR)</label>
                        </div>
                    </div>
                    <div class="col-md-6 col-xl-3">
                        <div class="form-check form-switch">
                            <input class="form-check-input" type="checkbox" id="check-duplicates" checked>
                            <label class="form-check-label" for="check-duplicates">Check duplicates before upload</label>
                        </div>
                    </div>
                    <div class="col-md-6 col-xl-3">
                        <div class="form-check form-switch">
                            <input class="form-check-input" type="checkbox" id="show-thumbnails">
                            <label class="form-check-label" for="show-thumbnails">Show series thumbnails</label>
                        </div>
                    </div>
                </div>
                <div class="form-text mt-2">
                    OCR runs sequentially on multiple sample images per series (capped) to improve burned-in text detection.
                </div>
            </details>
        </div>
    </div>

    <div id="selection-summary" class="alert alert-secondary py-2 mb-3 d-none"></div>
    <div class="d-flex flex-wrap align-items-center gap-2 mb-2">
        <div id="duplicate-progress" class="small text-muted"></div>
        <button type="button" id="deselect-duplicates" class="btn btn-sm btn-outline-danger d-none">
            <i class="bi bi-x-circle me-1"></i>Deselect all duplicates
        </button>
    </div>

    <details id="ocr-debug-panel" class="mb-3">
        <summary class="fw-semibold">OCR Debug Panel</summary>
        <div id="ocr-debug-content" class="small text-muted mt-2">OCR has not run yet.</div>
    </details>

    <div class="alert alert-warning d-none anon-alert" role="alert">
        <p class="mb-2">Potential anonymisation issues were detected. Flagged series are highlighted below.</p>
        <div id="anon-issue-summary" class="small"></div>
        <details>
            <summary>Override anonymisation check</summary>
            <p class="mt-2 mb-2">Only override if you are certain the warnings are false positives.</p>
            <button id="override-anon" class="btn btn-sm btn-outline-danger" type="button">Override and enable upload</button>
        </details>
    </div>

    <div id="preupload-grouped" class="mb-4">
        <div class="alert alert-info">Choose a folder to preview studies and series before upload.</div>
    </div>

    <div class="card mb-3">
        <div class="card-header d-flex justify-content-between align-items-center gap-2">
            <span class="fw-semibold">Pre-upload Viewer</span>
            <div class="ms-auto d-flex gap-2">
                <button type="button" class="btn btn-sm btn-outline-secondary" id="viewer-tags-btn" disabled>
                    <i class="bi bi-tags me-1"></i>DICOM Tags
                </button>
                <small id="viewer-context" class="text-muted">No study/series selected</small>
            </div>
        </div>
        <div class="card-body viewer-preupload-shell">
            <div id="uploadviewer" class="dicom-viewer-root viewer-preupload-root"></div>
        </div>
    </div>

    <!-- DICOM Tags Modal -->
    <div class="modal fade" id="dicomTagsModal" tabindex="-1">
        <div class="modal-dialog modal-lg modal-dialog-scrollable">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">DICOM Tags</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div class="mb-2">
                        <input id="tags-filter" class="form-control form-control-sm" type="search" placeholder="Filter tags (key or value)...">
                    </div>
                    <div id="tags-warning" class="alert alert-warning small mb-2" style="display:none;">
                        <strong>PHI Alert:</strong> <span id="tags-warning-text"></span>
                    </div>
                    <table class="table table-sm">
                        <tbody id="tags-tbody">
                            <tr><td colspan="2" class="text-muted">No tags loaded</td></tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>

    <details id="upload-results">
        <summary class="fw-semibold">Upload Results</summary>
        <div id="upload-results-list" class="pt-2">
            <h5>Uploaded</h5>
            <ul id="uploaded-files"></ul>
            <h5>Duplicates</h5>
            <ul id="duplicate-series"></ul>
            <details>
                <summary>Files</summary>
                <ul id="duplicate-files"></ul>
            </details>
            <h5>Failed</h5>
            <ul id="failed-files"></ul>
        </div>
    </details>

    <div id="loading" class="fullscreen-overlay" style="display:none;">
        <div class="progress-block">
            <div id="progress"></div>
            <div class="sk-cube-grid full">
                <div class="sk-cube sk-cube1"></div>
                <div class="sk-cube sk-cube2"></div>
                <div class="sk-cube sk-cube3"></div>
                <div class="sk-cube sk-cube4"></div>
                <div class="sk-cube sk-cube5"></div>
                <div class="sk-cube sk-cube6"></div>
                <div class="sk-cube sk-cube7"></div>
                <div class="sk-cube sk-cube8"></div>
                <div class="sk-cube sk-cube9"></div>
            </div>
            <div id="progress-text">Processing...</div>
        </div>
    </div>
{% endblock %}

{% block js %}
    <!-- Set skip flag EARLY before any other scripts load to prevent old Cornerstone viewer from running -->
    <script>
        window.skipLoadDicomViewer = true;
        console.log('skipLoadDicomViewer flag set before module scripts load');
    </script>

    <script src="https://cdn.jsdelivr.net/npm/hash-wasm@4"></script>
    <script src="{% static 'js/upload_form_helpers.js' %}"></script>
    <script src="{% static 'dv3d/index.js' %}" type="module" defer="defer"></script>
    <script>
        window.uploadPreview = {
            records: [],
            grouped: {},
            selectedPaths: new Set(),
            duplicateSeriesUrls: new Set(),
            forceAnonymisationOverride: false,
            ocrWorker: null,
            ocrDebug: null,
            currentTagsRecord: null,
            duplicateCheckInProgress: false,
            duplicateCheckRunId: 0,
            duplicateCheckTimer: null,
        };

        const SITE_PREFIXES = ["ref", "rk9", "ra9", "rh8", "rbz", "rba"];
        const OCR_SERIES_SAMPLE_COUNT = 6;
        const OCR_MAX_SAMPLES = 180;
        const OCR_CANVAS_MAX = 768;
        const DUPLICATE_BATCH_SIZE = 500;

        function setProgress(text) {
            document.getElementById("progress-text").textContent = text;
        }

        function showLoading(show, text) {
            const loading = document.getElementById("loading");
            if (!loading) return;
            loading.style.display = show ? "block" : "none";
            if (text) setProgress(text);
        }

        function resetUploadResults() {
            document.getElementById("uploaded-files").innerHTML = "";
            document.getElementById("duplicate-files").innerHTML = "";
            document.getElementById("duplicate-series").innerHTML = "";
            document.getElementById("failed-files").innerHTML = "";
            window.uploadPreview.duplicateSeriesUrls = new Set();
        }

        function normaliseTag(value) {
            if (value === undefined || value === null) return "";
            return String(value).trim();
        }

        function escapeHtml(value) {
            const div = document.createElement("div");
            div.textContent = value === undefined || value === null ? "" : String(value);
            return div.innerHTML;
        }

        function isOcrEnabled() {
            return !!document.getElementById("check-anonymisation-ocr")?.checked;
        }

        function syncOcrDebugPanelVisibility() {
            const panel = document.getElementById("ocr-debug-panel");
            if (!panel) return;
            const show = isOcrEnabled();
            panel.classList.toggle("d-none", !show);
            if (!show) {
                panel.removeAttribute("open");
            }
        }

        function resetOcrDebug(status = "idle", message = "OCR has not run yet.") {
            window.uploadPreview.ocrDebug = {
                status,
                message,
                totalSamples: 0,
                processedSamples: 0,
                flaggedSeriesCount: 0,
                entries: [],
                updatedAt: new Date().toISOString(),
            };
            renderOcrDebugPanel();
        }

        function renderOcrDebugPanel() {
            const root = document.getElementById("ocr-debug-content");
            if (!root) return;

            const debug = window.uploadPreview.ocrDebug;
            if (!debug) {
                root.innerHTML = "OCR has not run yet.";
                return;
            }

            const sampleRows = debug.entries.slice(-30).map((entry) => {
                const reasons = entry.reasons?.length ? escapeHtml(entry.reasons.join(", ")) : "No PHI pattern matched";
                return `
                    <tr>
                        <td>${escapeHtml(entry.seriesUid)}</td>
                        <td>${escapeHtml(entry.path)}</td>
                        <td>${entry.flagged ? '<span class="text-warning fw-semibold">Flagged</span>' : '<span class="text-success">Clear</span>'}</td>
                        <td>${reasons}</td>
                        <td><code>${escapeHtml(entry.preview)}</code></td>
                    </tr>
                `;
            }).join("");

            root.innerHTML = `
                <div class="mb-2">
                    <strong>Status:</strong> ${escapeHtml(debug.status)}<br>
                    <strong>Message:</strong> ${escapeHtml(debug.message)}<br>
                    <strong>Samples:</strong> ${debug.processedSamples}/${debug.totalSamples} · <strong>Flagged series:</strong> ${debug.flaggedSeriesCount}
                </div>
                ${sampleRows ? `
            <div class="table-responsive ocr-debug-table-wrap">
            <table class="table table-sm align-middle ocr-debug-table">
            <thead>
            <tr>
            <th>Series UID</th>
            <th>File</th>
            <th>Result</th>
            <th>Match reasons</th>
            <th>OCR preview</th>
            </tr>
            </thead>
            <tbody>${sampleRows}</tbody>
            </table>
            </div>
            ` : ""}
            `;
        }

        async function parseDicomMetadata(file) {
            try {
                const buffer = await file.arrayBuffer();
                const byteArray = new Uint8Array(buffer);
                const dataSet = dicomParser.parseDicom(byteArray);

                const studyUid = normaliseTag(dataSet.string("x0020000d")) || "unknown-study";
                const studyDescription = normaliseTag(dataSet.string("x00081030")) || "(no study description)";
                const studyDate = normaliseTag(dataSet.string("x00080020"));
                const seriesUid = normaliseTag(dataSet.string("x0020000e"));
                const seriesDescription = normaliseTag(dataSet.string("x0008103e")) || "(no series description)";
                const seriesNumber = normaliseTag(dataSet.string("x00200011"));
                const modality = normaliseTag(dataSet.string("x00080060"));
                const accessionNumber = normaliseTag(dataSet.string("x00080050")).toLowerCase();
                const patientId = normaliseTag(dataSet.string("x00100020")).toLowerCase();

                if (!seriesUid) return null;

                return {
                    file,
                    path: file.webkitRelativePath || file.name,
                    studyUid,
                    studyDescription,
                    studyDate,
                    seriesUid,
                    seriesDescription,
                    seriesNumber,
                    modality,
                    accessionNumber,
                    patientId,
                    include: true,
                    tagWarnings: [],
                    ocrWarning: false,
                    duplicate: false,
                    duplicateUrl: "",
                    duplicateChecked: false,
                    blake3Hash: "",
                    objectUrl: URL.createObjectURL(file),
                };
            } catch (err) {
                return null;
            }
        }

        function evaluateTagWarnings(record) {
            if (!document.getElementById("check-anonymisation-tags").checked) {
                record.tagWarnings = [];
                return;
            }

            const warnings = [];
            for (const prefix of SITE_PREFIXES) {
                if (record.accessionNumber && record.accessionNumber.startsWith(prefix)) {
                    warnings.push(`Accession begins with ${prefix} (${record.accessionNumber})`);
                }
                if (record.patientId && record.patientId.startsWith(prefix)) {
                    warnings.push(`Patient ID begins with ${prefix} (${record.patientId})`);
                }
            }
            record.tagWarnings = warnings;
        }

        function buildGrouping(records) {
            const grouped = {};
            for (const record of records) {
                if (!grouped[record.studyUid]) {
                    grouped[record.studyUid] = {
                        studyUid: record.studyUid,
                        description: record.studyDescription,
                        date: record.studyDate,
                        seriesMap: {},
                    };
                }
                const study = grouped[record.studyUid];
                if (!study.seriesMap[record.seriesUid]) {
                    study.seriesMap[record.seriesUid] = {
                        uid: record.seriesUid,
                        description: record.seriesDescription,
                        number: record.seriesNumber,
                        modality: record.modality,
                        files: [],
                    };
                }
                study.seriesMap[record.seriesUid].files.push(record);
            }
            return grouped;
        }

        function buildSelectionSummary() {
            const selectedRecords = window.uploadPreview.records.filter(r => r.include);
            const selectedSeries = new Set(selectedRecords.map(r => r.seriesUid));
            const selectedStudies = new Set(selectedRecords.map(r => r.studyUid));
            const summary = document.getElementById("selection-summary");
            if (!selectedRecords.length) {
                summary.classList.add("d-none");
                summary.innerHTML = "";
                return;
            }
            summary.classList.remove("d-none");
            summary.innerHTML = `${selectedRecords.length} files selected across ${selectedSeries.size} series in ${selectedStudies.size} studies.`;
        }

        function hasAnonymisationWarnings() {
            return window.uploadPreview.records.some((record) => {
                if (!record.include) return false;
                return record.tagWarnings.length > 0 || record.ocrWarning;
            });
        }

        function buildAnonymisationIssueSummary() {
            const selectedRecords = window.uploadPreview.records.filter((record) => record.include);
            if (!selectedRecords.length) {
                return null;
            }

            const seriesMap = {};
            let tagWarningFiles = 0;
            let ocrWarningFiles = 0;

            for (const record of selectedRecords) {
                const hasTagWarnings = record.tagWarnings.length > 0;
                const hasOcrWarning = !!record.ocrWarning;
                if (!hasTagWarnings && !hasOcrWarning) {
                    continue;
                }

                if (!seriesMap[record.seriesUid]) {
                    seriesMap[record.seriesUid] = {
                        uid: record.seriesUid,
                        description: record.seriesDescription || "(no series description)",
                        tagFiles: 0,
                        ocrFiles: 0,
                        details: new Set(),
                    };
                }

                const entry = seriesMap[record.seriesUid];
                if (hasTagWarnings) {
                    tagWarningFiles += 1;
                    entry.tagFiles += 1;
                    record.tagWarnings.forEach((warning) => entry.details.add(warning));
                }
                if (hasOcrWarning) {
                    ocrWarningFiles += 1;
                    entry.ocrFiles += 1;
                    entry.details.add("Possible burned-in patient text detected by OCR");
                }
            }

            const seriesIssues = Object.values(seriesMap);
            if (!seriesIssues.length) {
                return null;
            }

            seriesIssues.sort((a, b) => {
                const aScore = a.tagFiles + a.ocrFiles;
                const bScore = b.tagFiles + b.ocrFiles;
                return bScore - aScore;
            });

            return {
                selectedFiles: selectedRecords.length,
                flaggedSeries: seriesIssues.length,
                tagWarningFiles,
                ocrWarningFiles,
                issues: seriesIssues,
            };
        }

        function renderAnonymisationIssueSummary() {
            const summary = document.getElementById("anon-issue-summary");
            if (!summary) return;

            const data = buildAnonymisationIssueSummary();
            if (!data) {
                summary.innerHTML = "";
                return;
            }

            const issueItems = data.issues.slice(0, 8).map((issue) => {
                const issueBits = [];
                if (issue.tagFiles) issueBits.push(`Tag warnings in ${issue.tagFiles} file${issue.tagFiles === 1 ? "" : "s"}`);
                if (issue.ocrFiles) issueBits.push(`OCR warnings in ${issue.ocrFiles} file${issue.ocrFiles === 1 ? "" : "s"}`);
                const detailPreview = Array.from(issue.details).slice(0, 2).join("; ");

                return `
                    <li>
                        <strong>${escapeHtml(issue.description)}</strong>
                        <span class="text-muted">(${escapeHtml(issue.uid)})</span><br>
                        ${escapeHtml(issueBits.join(" \u00b7 "))}
                        ${detailPreview ? `<br><span class="text-muted">${escapeHtml(detailPreview)}</span>` : ""}
                    </li>
                `;
            }).join("");

            const extraCount = Math.max(0, data.issues.length - 8);
            const extraLine = extraCount ? `<p class="mb-0 mt-2 text-muted">+ ${extraCount} more flagged series</p>` : "";

            summary.innerHTML = `
                <p class="mb-2">
                    <strong>Summary:</strong>
                    ${data.flaggedSeries} flagged series across ${data.selectedFiles} selected files.
                    Tag warnings: ${data.tagWarningFiles}. OCR warnings: ${data.ocrWarningFiles}.
                </p>
                <ul class="mb-0 ps-3">
                    ${issueItems}
                </ul>
                ${extraLine}
            `;
        }

        function updateUploadButtonState() {
            const uploadButton = document.getElementById("uploadButton");
            const anonAlert = document.querySelector(".anon-alert");
            const deselectDuplicatesBtn = document.getElementById("deselect-duplicates");
            const hasSelected = window.uploadPreview.records.some(r => r.include);
            const hasWarnings = hasAnonymisationWarnings();
            const hasDuplicates = window.uploadPreview.records.some(r => r.include && r.duplicate);
            const duplicatesEnabled = document.getElementById("check-duplicates")?.checked;
            const hasPendingDuplicateChecks = duplicatesEnabled && hasSelected &&
            window.uploadPreview.records.some((r) => r.include && !r.duplicateChecked);

            renderAnonymisationIssueSummary();
            if (deselectDuplicatesBtn) {
                deselectDuplicatesBtn.classList.toggle("d-none", !hasDuplicates);
            }

            if (!hasSelected) {
                uploadButton.disabled = true;
                anonAlert.classList.add("d-none");
                return;
            }

            if (hasWarnings && !window.uploadPreview.forceAnonymisationOverride) {
                uploadButton.disabled = true;
                anonAlert.classList.remove("d-none");
                return;
            }

            if (hasPendingDuplicateChecks || window.uploadPreview.duplicateCheckInProgress) {
                uploadButton.disabled = true;
                anonAlert.classList.add("d-none");
                return;
            }

            anonAlert.classList.add("d-none");
            uploadButton.disabled = !!hasDuplicates;
        }

        function getSeriesDuplicateCheckBadge(series) {
            const duplicatesEnabled = document.getElementById("check-duplicates")?.checked;
            if (!duplicatesEnabled) {
                return '<span class="badge text-bg-secondary">Duplicate check off</span>';
            }

            const selectedFiles = series.files.filter((f) => f.include);
            if (!selectedFiles.length) {
                return '<span class="badge text-bg-secondary">Not selected</span>';
            }

            const checkedCount = selectedFiles.filter((f) => f.duplicateChecked).length;
            if (checkedCount < selectedFiles.length) {
                const badgeClass = window.uploadPreview.duplicateCheckInProgress ? "text-bg-info" : "text-bg-warning";
                return `<span class="badge ${badgeClass}">Duplicate check: ${checkedCount}/${selectedFiles.length}</span>`;
            }

            return '<span class="badge text-bg-success">Duplicate check complete</span>';
        }

        function getSeriesStatusClass(series) {
            const includeCount = series.files.filter(f => f.include).length;
            if (!includeCount) return "series-card excluded";

            const hasTagWarnings = series.files.some(f => f.tagWarnings.length > 0);
            const hasOcrWarnings = series.files.some(f => f.ocrWarning);
            const hasDuplicates = series.files.some(f => f.include && f.duplicate);
            if (hasTagWarnings || hasOcrWarnings) return "series-card warning";
            if (hasDuplicates) return "series-card duplicate";
            return "series-card";
        }

        function deselectDuplicateSeries() {
            const duplicateSeriesUids = new Set(
                window.uploadPreview.records
                    .filter((record) => record.include && record.duplicate)
                    .map((record) => record.seriesUid)
            );

            if (!duplicateSeriesUids.size) {
                toastr.info("No selected duplicate series to deselect.");
                return;
            }

            duplicateSeriesUids.forEach((seriesUid) => applyIncludeToSeries(seriesUid, false));
            window.uploadPreview.forceAnonymisationOverride = false;
            scheduleDuplicateCheck();
            renderGroupedPreview();
            toastr.info(`Deselected ${duplicateSeriesUids.size} duplicate series.`);
        }

        function renderGroupedPreview() {
            const groupedRoot = document.getElementById("preupload-grouped");
            const grouped = window.uploadPreview.grouped;
            const studyKeys = Object.keys(grouped);

            if (!studyKeys.length) {
                groupedRoot.innerHTML = '<div class="alert alert-info">No valid DICOM files were found in the selected folder.</div>';
                buildSelectionSummary();
                updateUploadButtonState();
                return;
            }

            const html = studyKeys.map((studyUid) => {
                const study = grouped[studyUid];
                const seriesList = Object.values(study.seriesMap);

                const seriesHtml = seriesList.map((series) => {
                    const includeCount = series.files.filter(f => f.include).length;
                    const tagWarningCount = series.files.filter(f => f.tagWarnings.length > 0).length;
                    const ocrWarningCount = series.files.filter(f => f.ocrWarning).length;
                    const duplicateCount = series.files.filter(f => f.duplicate).length;

                    return `
                        <li class="${getSeriesStatusClass(series)}" data-series-uid="${series.uid}">
                            <div class="series-header">
                                <div>
                                    <h6 class="mb-1">${series.description || "(no description)"}</h6>
                                    <div class="small text-muted">
                                        UID: ${series.uid}<br>
                                        ${series.modality ? `Modality: ${series.modality} · ` : ""}
                                        ${series.number ? `Series #: ${series.number} · ` : ""}
                                        Files: ${series.files.length}
                                    </div>
                                </div>
                                <div class="series-actions">
                                    <div class="form-check form-switch mb-2">
                                        <input class="form-check-input include-series" type="checkbox" data-series-uid="${series.uid}" ${includeCount ? "checked" : ""}>
                                        <label class="form-check-label small">Include</label>
                                    </div>
                                    <button type="button" class="btn btn-sm btn-outline-secondary view-series-btn" data-series-uid="${series.uid}">View series</button>
                                    <button type="button" class="btn btn-sm btn-outline-secondary view-study-btn" data-study-uid="${studyUid}">View study</button>
                                </div>
                            </div>
                            <div class="series-flags mt-2 small">
                                ${tagWarningCount ? `<span class="badge text-bg-warning">Tag warnings: ${tagWarningCount}</span>` : ""}
                                ${ocrWarningCount ? `<span class="badge text-bg-warning">OCR warnings: ${ocrWarningCount}</span>` : ""}
                                ${getSeriesDuplicateCheckBadge(series)}
                                ${duplicateCount ? `<span class="badge text-bg-danger">Duplicates: ${duplicateCount}</span>` : ""}
                            </div>
                            <div class="series-thumbnail mt-2" data-series-thumb="${series.uid}"></div>
                        </li>
                    `;
                }).join("");

                return `
                    <details class="study-block" open>
                        <summary>
                            <strong>${study.description || "(no study description)"}</strong>
                            <small class="text-muted">(${studyUid}) — ${seriesList.length} series${study.date ? ` — ${study.date}` : ""}</small>
                        </summary>
                        <div class="study-controls d-flex flex-wrap gap-2 mt-2">
                            <button type="button" class="btn btn-sm btn-outline-secondary include-study" data-study-uid="${studyUid}" data-value="1">Include all in study</button>
                            <button type="button" class="btn btn-sm btn-outline-secondary include-study" data-study-uid="${studyUid}" data-value="0">Exclude all in study</button>
                            <button type="button" class="btn btn-sm btn-outline-secondary view-study-btn" data-study-uid="${studyUid}">View study</button>
                        </div>
                        <ul class="study-series-list mt-2">${seriesHtml}</ul>
                    </details>
                `;
            }).join("");

            groupedRoot.innerHTML = html;

            if (document.getElementById("show-thumbnails").checked) {
                renderSeriesThumbnails();
            }

            buildSelectionSummary();
            updateUploadButtonState();
        }

        async function renderSeriesThumbnails() {
            const seriesMap = {};
            for (const record of window.uploadPreview.records) {
                if (!seriesMap[record.seriesUid]) seriesMap[record.seriesUid] = record;
            }

            for (const seriesUid of Object.keys(seriesMap)) {
                const holder = document.querySelector(`[data-series-thumb="${seriesUid}"]`);
                if (!holder) continue;
                const record = seriesMap[seriesUid];

                try {
                    const imgDataUrl = await renderDicomToDataUrl(record.file, 160);
                    holder.innerHTML = `<img src="${imgDataUrl}" alt="Series thumbnail" class="thumb-preview">`;
                } catch (err) {
                    holder.innerHTML = '<span class="text-muted small">Thumbnail unavailable</span>';
                }
            }
        }

        async function renderDicomToDataUrl(file, maxSize) {
            const element = document.createElement("div");
            element.style.width = `${maxSize}px`;
            element.style.height = `${maxSize}px`;
            element.style.position = "fixed";
            element.style.left = "-10000px";
            element.style.top = "-10000px";
            document.body.appendChild(element);

            const objectUrl = URL.createObjectURL(file);
            const imageId = `wadouri:${objectUrl}`;

            try {
                cornerstone.enable(element);
                const image = await cornerstone.loadAndCacheImage(imageId);
                cornerstone.displayImage(element, image);
                const canvas = element.querySelector("canvas");
                if (!canvas) throw new Error("No canvas");

                const outCanvas = document.createElement("canvas");
                const scale = Math.min(maxSize / canvas.width, maxSize / canvas.height, 1);
                outCanvas.width = Math.max(1, Math.round(canvas.width * scale));
                outCanvas.height = Math.max(1, Math.round(canvas.height * scale));
                const ctx = outCanvas.getContext("2d", { alpha: false });
                ctx.drawImage(canvas, 0, 0, outCanvas.width, outCanvas.height);

                return outCanvas.toDataURL("image/jpeg", 0.75);
            } finally {
                try { cornerstone.disable(element); } catch (err) {}
                URL.revokeObjectURL(objectUrl);
                element.remove();
                try { cornerstone.imageCache.purgeCache(); } catch (err) {}
            }
        }

        async function ensureOcrWorker() {
            if (window.uploadPreview.ocrWorker) return window.uploadPreview.ocrWorker;
            window.uploadPreview.ocrWorker = await Tesseract.createWorker("eng");
            try {
                await window.uploadPreview.ocrWorker.setParameters({
                    tessedit_pageseg_mode: "6",
                    preserve_interword_spaces: "1",
                });
            } catch (err) {}
            return window.uploadPreview.ocrWorker;
        }

        async function waitForPaint() {
            await new Promise((resolve) => requestAnimationFrame(resolve));
            await new Promise((resolve) => requestAnimationFrame(resolve));
        }

        function getOcrPhiReasons(text) {
            const value = (text || "").toLowerCase();
            const reasons = [];

            // Catch common clinical identifier markers and identifier-like tokens.
            if (value.includes("accession")) reasons.push("contains 'accession'");
            if (value.includes("patient")) reasons.push("contains 'patient'");
            if (value.includes("nhs")) reasons.push("contains 'nhs'");
            if (value.includes("hospital")) reasons.push("contains 'hospital'");

            if (/(ref|rk9|rh8|ra9|rbz|rba)\d+/i.test(value)) reasons.push("site prefix + number pattern");
            if (/\b\d{3}[\s-]?\d{3}[\s-]?\d{4}\b/.test(value)) reasons.push("10-digit grouped number pattern");
            if (/\b\d{10}\b/.test(value)) reasons.push("10-digit number pattern");
            if (/\b\d{6,8}\b/.test(value)) reasons.push("6-8 digit number pattern");
            if (/\b(?:dob|d\.o\.b|birth|born)\b/.test(value)) reasons.push("date-of-birth keyword");

            return reasons;
        }

        function detectOcrPhi(text) {
            return getOcrPhiReasons(text).length > 0;
        }

        function buildOcrSamples(records) {
            const includeRecords = records.filter((record) => record.include);
            const seriesMap = {};
            for (const record of includeRecords) {
                if (!seriesMap[record.seriesUid]) {
                    seriesMap[record.seriesUid] = [];
                }
                seriesMap[record.seriesUid].push(record);
            }

            const samples = [];
            for (const seriesRecords of Object.values(seriesMap)) {
                if (!seriesRecords.length) continue;
                const sampleCount = Math.min(OCR_SERIES_SAMPLE_COUNT, seriesRecords.length);
                const used = new Set();
                for (let i = 0; i < sampleCount; i++) {
                    const index = sampleCount === 1
                        ? 0
                        : Math.floor((i * (seriesRecords.length - 1)) / (sampleCount - 1));
                    if (used.has(index)) continue;
                    used.add(index);
                    samples.push(seriesRecords[index]);
                }
            }

            return samples.slice(0, OCR_MAX_SAMPLES);
        }

        function normaliseOcrText(text) {
            return String(text || "").replace(/\s+/g, " ").trim();
        }

        function buildEnhancedOcrCanvas(sourceCanvas) {
            const enhanced = document.createElement("canvas");
            enhanced.width = sourceCanvas.width;
            enhanced.height = sourceCanvas.height;
            const ectx = enhanced.getContext("2d", { alpha: false });
            ectx.drawImage(sourceCanvas, 0, 0);

            const imageData = ectx.getImageData(0, 0, enhanced.width, enhanced.height);
            const data = imageData.data;

            let minLum = 255;
            let maxLum = 0;
            let totalLum = 0;
            for (let i = 0; i < data.length; i += 4) {
                const lum = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
                if (lum < minLum) minLum = lum;
                if (lum > maxLum) maxLum = lum;
                totalLum += lum;
            }

            const meanLum = totalLum / (data.length / 4 || 1);
            const invert = meanLum < 100;
            const range = Math.max(1, maxLum - minLum);

            for (let i = 0; i < data.length; i += 4) {
                let lum = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
                lum = ((lum - minLum) * 255) / range;
                if (invert) lum = 255 - lum;
                const bw = lum > 150 ? 255 : 0;
                data[i] = bw;
                data[i + 1] = bw;
                data[i + 2] = bw;
            }

            ectx.putImageData(imageData, 0, 0);
            return enhanced;
        }

        async function canvasToBlob(canvas, mimeType = "image/png") {
            return await new Promise((resolve, reject) => {
                canvas.toBlob((blob) => {
                    if (!blob) reject(new Error("Failed to create OCR blob"));
                    else resolve(blob);
                }, mimeType);
            });
        }

        async function getDownscaledOcrCanvases(file) {
            const element = document.createElement("div");
            element.style.width = "512px";
            element.style.height = "512px";
            element.style.position = "fixed";
            element.style.left = "-10000px";
            element.style.top = "-10000px";
            document.body.appendChild(element);

            const objectUrl = URL.createObjectURL(file);
            const imageId = `wadouri:${objectUrl}`;

            try {
                cornerstone.enable(element);
                const image = await cornerstone.loadAndCacheImage(imageId);
                cornerstone.displayImage(element, image);
                await waitForPaint();

                const sourceCanvas = element.querySelector("canvas");
                if (!sourceCanvas) throw new Error("No canvas");

                const outCanvas = document.createElement("canvas");
                const scale = Math.min(OCR_CANVAS_MAX / sourceCanvas.width, OCR_CANVAS_MAX / sourceCanvas.height, 1);
                outCanvas.width = Math.max(1, Math.round(sourceCanvas.width * scale));
                outCanvas.height = Math.max(1, Math.round(sourceCanvas.height * scale));
                const ctx = outCanvas.getContext("2d", { alpha: false });
                ctx.drawImage(sourceCanvas, 0, 0, outCanvas.width, outCanvas.height);

                const enhancedCanvas = buildEnhancedOcrCanvas(outCanvas);
                return { outCanvas, enhancedCanvas };
            } finally {
                try { cornerstone.disable(element); } catch (err) {}
                URL.revokeObjectURL(objectUrl);
                element.remove();
                try { cornerstone.imageCache.purgeCache(); } catch (err) {}
            }
        }

        function buildOcrCandidateCanvases(baseCanvas, enhancedCanvas) {
            const candidates = [
                { name: "full", canvas: baseCanvas },
                { name: "full-enhanced", canvas: enhancedCanvas },
            ];

            const makeCrop = (src, sx, sy, sw, sh) => {
                const c = document.createElement("canvas");
                c.width = Math.max(1, sw);
                c.height = Math.max(1, sh);
                const ctx = c.getContext("2d", { alpha: false });
                ctx.drawImage(src, sx, sy, sw, sh, 0, 0, c.width, c.height);
                return c;
            };

            const addRegions = (src, suffix) => {
                const w = src.width;
                const h = src.height;
                const headerH = Math.max(24, Math.floor(h * 0.23));
                const footerH = Math.max(24, Math.floor(h * 0.18));
                const cornerW = Math.max(32, Math.floor(w * 0.45));

                candidates.push({ name: `top-left${suffix}`, canvas: makeCrop(src, 0, 0, cornerW, headerH) });
                candidates.push({ name: `top-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), 0, cornerW, headerH) });
                candidates.push({ name: `bottom-left${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), cornerW, footerH) });
                candidates.push({ name: `bottom-right${suffix}`, canvas: makeCrop(src, Math.max(0, w - cornerW), Math.max(0, h - footerH), cornerW, footerH) });
                candidates.push({ name: `header-strip${suffix}`, canvas: makeCrop(src, 0, 0, w, headerH) });
                candidates.push({ name: `footer-strip${suffix}`, canvas: makeCrop(src, 0, Math.max(0, h - footerH), w, footerH) });
            };

            addRegions(baseCanvas, "");
            addRegions(enhancedCanvas, "-enhanced");
            return candidates;
        }

        async function runOcrChecks() {
            syncOcrDebugPanelVisibility();
            if (!document.getElementById("check-anonymisation-ocr").checked) {
                window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
                resetOcrDebug("disabled", "OCR check is disabled in Upload Settings.");
                return;
            }

            if (!window.Tesseract || typeof window.Tesseract.createWorker !== "function") {
                window.uploadPreview.records.forEach(r => { r.ocrWarning = false; });
                resetOcrDebug("error", "OCR engine is unavailable in this browser session.");
                toastr.error("OCR engine is unavailable in this browser session.");
                return;
            }

            const samples = buildOcrSamples(window.uploadPreview.records);
            if (!samples.length) {
                resetOcrDebug("idle", "No included files available for OCR sampling.");
                return;
            }

            window.uploadPreview.ocrDebug = {
                status: "running",
                message: "OCR checks in progress.",
                totalSamples: samples.length,
                processedSamples: 0,
                flaggedSeriesCount: 0,
                entries: [],
                updatedAt: new Date().toISOString(),
            };
            renderOcrDebugPanel();

            const worker = await ensureOcrWorker();
            let index = 0;
            const flaggedSeriesUids = new Set();

            for (const record of samples) {
                index += 1;
                setProgress(`OCR ${index} of ${samples.length}: ${record.path}`);
                try {
                    const { outCanvas, enhancedCanvas } = await getDownscaledOcrCanvases(record.file);
                    const ocrCandidates = buildOcrCandidateCanvases(outCanvas, enhancedCanvas);

                    let bestText = "";
                    let bestConfidence = 0;
                    let bestCandidateName = "none";
                    let bestReasons = [];

                    for (const candidate of ocrCandidates) {
                        const blob = await canvasToBlob(candidate.canvas);
                        const result = await worker.recognize(blob);
                        const text = normaliseOcrText(result?.data?.text || "");
                        const confidence = Number(result?.data?.confidence || 0);
                        const reasons = getOcrPhiReasons(text);

                        const isBetter = reasons.length > bestReasons.length
                        || (reasons.length === bestReasons.length && text.length > bestText.length)
                        || (reasons.length === bestReasons.length && text.length === bestText.length && confidence > bestConfidence);

                        if (isBetter) {
                            bestText = text;
                            bestConfidence = confidence;
                            bestCandidateName = candidate.name;
                            bestReasons = reasons;
                        }

                        if (reasons.length > 0 && confidence >= 40 && text.length >= 6) {
                            break;
                        }
                    }

                    const reasons = bestReasons;
                    const flagged = reasons.length > 0;
                    if (flagged) {
                        flaggedSeriesUids.add(record.seriesUid);
                    }

                    const preview = bestText ? bestText.slice(0, 180) : "[no text extracted]";
                    window.uploadPreview.ocrDebug.entries.push({
                        seriesUid: record.seriesUid,
                        path: record.path,
                        flagged,
                        reasons,
                        preview: `${preview} (region ${bestCandidateName}, conf ${Math.round(bestConfidence)}%)`,
                    });
                } catch (err) {
                    // Ignore per-image OCR errors and continue with other samples.
                    window.uploadPreview.ocrDebug.entries.push({
                        seriesUid: record.seriesUid,
                        path: record.path,
                        flagged: false,
                        reasons: ["OCR error"],
                        preview: String(err?.message || "OCR failed"),
                    });
                } finally {
                    window.uploadPreview.ocrDebug.processedSamples = index;
                    window.uploadPreview.ocrDebug.flaggedSeriesCount = flaggedSeriesUids.size;
                    window.uploadPreview.ocrDebug.updatedAt = new Date().toISOString();
                    renderOcrDebugPanel();
                }
            }

            window.uploadPreview.records.forEach((record) => {
                record.ocrWarning = record.include && flaggedSeriesUids.has(record.seriesUid);
            });

            window.uploadPreview.ocrDebug.status = "complete";
            window.uploadPreview.ocrDebug.message = `OCR complete. ${flaggedSeriesUids.size} series flagged from ${samples.length} sampled image(s).`;
            window.uploadPreview.ocrDebug.flaggedSeriesCount = flaggedSeriesUids.size;
            window.uploadPreview.ocrDebug.updatedAt = new Date().toISOString();
            renderOcrDebugPanel();
        }

        async function calculateBlake3Hash(file) {
            const buffer = await file.arrayBuffer();
            try {
                const byteArray = new Uint8Array(buffer);
                const dataSet = dicomParser.parseDicom(byteArray);
                const pixelDataElement = dataSet.elements.x7fe00010;
                if (!pixelDataElement) throw new Error("No Pixel Data");
                const pixelData = byteArray.subarray(pixelDataElement.dataOffset, pixelDataElement.dataOffset + pixelDataElement.length);
                return await hashwasm.blake3(pixelData);
            } catch (err) {
                return await hashwasm.blake3(new Uint8Array(buffer));
            }
        }

        function scheduleDuplicateCheck() {
            if (window.uploadPreview.duplicateCheckTimer) {
                clearTimeout(window.uploadPreview.duplicateCheckTimer);
            }
            window.uploadPreview.duplicateCheckTimer = setTimeout(async () => {
                await processDuplicates();
                renderGroupedPreview();
            }, 100);
        }

        async function processDuplicates() {
            const dupProgress = document.getElementById("duplicate-progress");
            dupProgress.innerHTML = "";
            window.uploadPreview.records.forEach((r) => {
                r.duplicate = false;
                r.duplicateUrl = "";
                if (r.include) r.duplicateChecked = false;
            });
            document.getElementById("duplicate-series").innerHTML = "";
            window.uploadPreview.duplicateSeriesUrls = new Set();

            if (!document.getElementById("check-duplicates").checked) {
                window.uploadPreview.records.forEach((r) => {
                    r.duplicateChecked = true;
                });
                window.uploadPreview.duplicateCheckInProgress = false;
                updateUploadButtonState();
                return;
            }

            const selected = window.uploadPreview.records.filter((r) => r.include);
            if (!selected.length) {
                window.uploadPreview.duplicateCheckInProgress = false;
                updateUploadButtonState();
                return;
            }

            const runId = window.uploadPreview.duplicateCheckRunId + 1;
            window.uploadPreview.duplicateCheckRunId = runId;
            window.uploadPreview.duplicateCheckInProgress = true;
            updateUploadButtonState();

            let processed = 0;
            const duplicateSeriesMap = {};

            try {
                for (let offset = 0; offset < selected.length; offset += DUPLICATE_BATCH_SIZE) {
                    if (runId !== window.uploadPreview.duplicateCheckRunId) return;

                    const batch = selected.slice(offset, offset + DUPLICATE_BATCH_SIZE);
                    const hashes = [];

                    for (const record of batch) {
                        if (runId !== window.uploadPreview.duplicateCheckRunId) return;
                        if (!record.blake3Hash) {
                            setProgress(`Hashing ${processed + 1} of ${selected.length}: ${record.path}`);
                            record.blake3Hash = await calculateBlake3Hash(record.file);
                        }
                        hashes.push(record.blake3Hash);
                        processed += 1;
                    }

                    dupProgress.innerHTML = `<span class="text-info">Checking duplicate batch ${Math.floor(offset / DUPLICATE_BATCH_SIZE) + 1} of ${Math.ceil(selected.length / DUPLICATE_BATCH_SIZE)} (${processed}/${selected.length})...</span>`;

                    let result = {};
                    try {
                        const controller = new AbortController();
                        const timeoutId = setTimeout(() => controller.abort(), 30000);
                        const res = await fetch("{% url 'api-1:check_images_hashes' %}", {
                            method: "POST",
                            headers: {
                                "Content-Type": "application/json",
                                "X-CSRFToken": "{{ csrf_token }}",
                            },
                            body: JSON.stringify(hashes),
                            signal: controller.signal,
                        });
                        clearTimeout(timeoutId);
                        if (res.ok) result = await res.json();
                        else throw new Error(`Server returned ${res.status}`);
                    } catch (err) {
                        if (err.name === "AbortError") {
                            dupProgress.innerHTML = '<span class="text-warning">Duplicate check batch timed out. Upload is locked until checks complete.</span>';
                        } else {
                            console.error(err);
                            dupProgress.innerHTML = '<span class="text-warning">Duplicate check failed. Upload is locked until checks complete.</span>';
                        }
                        return;
                    }

                    for (const record of batch) {
                        const info = result[record.blake3Hash];
                        if (info && info.id) {
                            record.duplicate = true;
                            if (info.url) {
                                record.duplicateUrl = info.url;
                                duplicateSeriesMap[info.url] = info.id;
                            }
                        }
                        record.duplicateChecked = true;
                    }

                    renderGroupedPreview();
                }
            } finally {
                if (runId === window.uploadPreview.duplicateCheckRunId) {
                    window.uploadPreview.duplicateCheckInProgress = false;
                }
            }

            if (runId !== window.uploadPreview.duplicateCheckRunId) {
                return;
            }

            if (Object.keys(duplicateSeriesMap).length) {
                dupProgress.innerHTML = `<span class="text-danger fw-semibold">Duplicates detected. Remove duplicates before upload.</span>`;
                const duplicateSeries = document.getElementById("duplicate-series");
                Object.entries(duplicateSeriesMap).forEach(([url, id]) => {
                    const item = document.createElement("li");
                    item.innerHTML = `<a href="${url}" target="_blank">Series ${id}</a>`;
                    duplicateSeries.appendChild(item);
                });
            } else {
                dupProgress.innerHTML = '<span class="text-success">All duplicate checks complete. No duplicates detected.</span>';
            }

            updateUploadButtonState();
        }

        function collectSelectedRecords() {
            return window.uploadPreview.records.filter(r => r.include);
        }

        function getSeriesRecords(seriesUid) {
            return window.uploadPreview.records.filter(r => r.seriesUid === seriesUid && r.include);
        }

        function getStudyRecords(studyUid) {
            return window.uploadPreview.records.filter(r => r.studyUid === studyUid && r.include);
        }

        async function showRecordsInViewer(records, label) {
            if (!records.length) return;
            console.log('showRecordsInViewer called with', records.length, 'records, label:', label);
            // Pass the File objects directly to the viewer
            const files = records.map((record) => record.file);
            console.log('Files to load:', files);
            window.uploadPreview.currentTagsRecord = records[0];

            document.getElementById("viewer-context").textContent = label;
            document.getElementById("viewer-tags-btn").disabled = false;

            // Load files directly into the viewer
            await loadDv3dViewer(files);

            // Focus the viewer card
            const viewerCard = document.querySelector(".card-header:has(#viewer-context)").closest(".card");
            if (viewerCard) {
                viewerCard.scrollIntoView({ behavior: "smooth", block: "nearest" });
            }
        }

        function applyIncludeToSeries(seriesUid, include) {
            window.uploadPreview.records.forEach((record) => {
                if (record.seriesUid === seriesUid) record.include = include;
            });
        }

        function applyIncludeToStudy(studyUid, include) {
            window.uploadPreview.records.forEach((record) => {
                if (record.studyUid === studyUid) record.include = include;
            });
        }

        async function onFilesChanged() {
            const fileInput = document.getElementById("filepicker");
            const files = Array.from(fileInput.files || []);

            window.uploadPreview.forceAnonymisationOverride = false;
            resetUploadResults();
            resetOcrDebug();
            syncOcrDebugPanelVisibility();

            window.uploadPreview.records.forEach((r) => {
                if (r.objectUrl) URL.revokeObjectURL(r.objectUrl);
            });
            window.uploadPreview.records = [];
            window.uploadPreview.grouped = {};

            if (!files.length) {
                renderGroupedPreview();
                return;
            }

            showLoading(true, "Scanning DICOM files...");
            let i = 0;
            for (const file of files) {
                i += 1;
                setProgress(`Scanning ${i} of ${files.length}: ${file.webkitRelativePath || file.name}`);
                const record = await parseDicomMetadata(file);
                if (!record) continue;
                evaluateTagWarnings(record);
                window.uploadPreview.records.push(record);
            }

            window.uploadPreview.grouped = buildGrouping(window.uploadPreview.records);
            renderGroupedPreview();

            await runOcrChecks();
            renderGroupedPreview();
            showLoading(false);
            scheduleDuplicateCheck();
        }

        function* chunks(arr, n) {
            for (let i = 0; i < arr.length; i += n) {
                yield arr.slice(i, i + n);
            }
        }

        function uploadChunk(filesToUpload, currentChunk, totalChunks) {
            return new Promise((resolve, reject) => {
                const formData = new FormData();
                filesToUpload.forEach((record) => formData.append("files", record.file));

                const xhr = new XMLHttpRequest();
                xhr.open("POST", "{% url 'api-1:upload_dicom' %}", true);
                xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");

                xhr.onreadystatechange = function () {
                    if (xhr.readyState !== XMLHttpRequest.DONE) return;

                    if (xhr.status !== 200) {
                        reject(new Error("Upload failed"));
                        return;
                    }

                    let res;
                    try {
                        res = JSON.parse(xhr.responseText || xhr.response);
                    } catch (err) {
                        reject(new Error("Invalid JSON response"));
                        return;
                    }

                    if (res.uploaded && res.uploaded.length) {
                        const uploadedFiles = document.getElementById("uploaded-files");
                        res.uploaded.forEach((u) => {
                            const li = document.createElement("li");
                            li.innerHTML = `${u[0]}` + (u[1] ? ` <small class="text-muted">(${u[1]})</small>` : "");
                            uploadedFiles.appendChild(li);
                        });
                    }

                    if (res.duplicates && res.duplicates.length) {
                        const duplicateFiles = document.getElementById("duplicate-files");
                        res.duplicates.forEach((d) => {
                            const li = document.createElement("li");
                            li.innerHTML = `${d[0]}` + (d[1] ? ` <small class="text-muted">(${d[1]})</small>` : "");
                            duplicateFiles.appendChild(li);
                        });
                    }

                    if (res.failed && res.failed.length) {
                        const failedFiles = document.getElementById("failed-files");
                        res.failed.forEach((f) => {
                            const li = document.createElement("li");
                            li.textContent = f;
                            failedFiles.appendChild(li);
                        });
                    }

                    if (res.duplicate_series && res.duplicate_series.length) {
                        const duplicateSeries = document.getElementById("duplicate-series");
                        res.duplicate_series.forEach((url) => {
                            if (window.uploadPreview.duplicateSeriesUrls.has(url)) return;
                            window.uploadPreview.duplicateSeriesUrls.add(url);
                            const li = document.createElement("li");
                            li.innerHTML = `<a href="${url}" target="_blank">${url}</a>`;
                            duplicateSeries.appendChild(li);
                        });
                    }

                    resolve();
                };

                setProgress(`Uploading chunk ${currentChunk + 1} of ${totalChunks}`);
                xhr.send(formData);
            });
        }

        async function onUploadButtonClicked(event) {
            event.preventDefault();
            const selected = collectSelectedRecords();

            if (!selected.length) {
                alert("Please select at least one DICOM file to upload.");
                return;
            }

            if (hasAnonymisationWarnings() && !window.uploadPreview.forceAnonymisationOverride) {
                alert("Anonymisation warnings must be resolved or overridden before uploading.");
                return;
            }

            if (document.getElementById("check-duplicates").checked &&
                window.uploadPreview.records.some((r) => r.include && !r.duplicateChecked)) {
                    alert("Duplicate checks are still running or incomplete. Please wait for all checks to finish before uploading.");
                    return;
                }

            resetUploadResults();
            showLoading(true, "Uploading...");

            try {
                const chunked = [...chunks(selected, 10)];
                for (let i = 0; i < chunked.length; i++) {
                    await uploadChunk(chunked[i], i, chunked.length);
                }
                document.getElementById("upload-results").open = true;
                toastr.success("Upload complete");
            } catch (err) {
                console.error(err);
                toastr.error("Upload failed. Please retry.");
            } finally {
                showLoading(false);
            }
        }

        function bindIfExists(id, eventName, handler) {
            const element = document.getElementById(id);
            if (element) element.addEventListener(eventName, handler);
        }

        async function loadDv3dViewer(files) {
            try {
                console.log('loadDv3dViewer called with', files.length, 'files');

                // Wait for dv3d viewer API to be available
                if (!window.loadDicomStackFromFiles && !window.loadDicomStackFromWadouriList) {
                    console.log('Waiting for dv3d viewer API to load...');
                    await new Promise((resolve) => {
                        const checkInterval = setInterval(() => {
                            if (window.loadDicomStackFromFiles || window.loadDicomStackFromWadouriList) {
                                clearInterval(checkInterval);
                                resolve();
                            }
                        }, 100);
                        setTimeout(() => clearInterval(checkInterval), 5000);
                    });
                }

                const viewerElement = document.getElementById('uploadviewer');
                if (!viewerElement) {
                    console.warn('Viewer element not found');
                    return;
                }

                //console.log('Clearing viewer element and loading files');
                //// Clear any old viewer content before mounting dv3d
                //viewerElement.innerHTML = '';
                if (window.clearViewer_uploadviewer && typeof window.clearViewer_uploadviewer === 'function') {
                    console.log('Clearing viewer using clearViewer_uploadviewer');
                    //window.clearViewer_uploadviewer();
                }

                // Try to load files using the dv3d viewer API
                if (window.loadDicomStackFromFiles && typeof window.loadDicomStackFromFiles === 'function') {
                    console.log('Using loadDicomStackFromFiles API');
                    window.loadDicomStackFromFiles(files);
                    console.log('Files loaded into viewer');
                } else if (window.loadDicomStackFromWadouriList && typeof window.loadDicomStackFromWadouriList === 'function') {
                    console.log('Using loadDicomStackFromWadouriList API');
                    const wadouriList = files.map(f => 'wadouri:' + URL.createObjectURL(f));
                    window.loadDicomStackFromWadouriList(wadouriList);
                    console.log('WADO URIs loaded into viewer');
                } else {
                    console.warn('Neither loadDicomStackFromFiles nor loadDicomStackFromWadouriList available');
                }
            } catch (err) {
                console.error('Failed to load dv3d viewer:', err);
            }
        }

        const PHI_TAGS = [
            "x00080050",  // Accession Number
            "x00080020",  // Study Date
            "x00100010",  // Patient Name
            "x00100020",  // Patient ID
            "x00100030",  // Patient Birth Date
            "x00100040",  // Patient Sex
            "x0010101b",  // Patient Address
            "x00101010",  // Patient Age
            "x00101040",  // Patient Telephone Numbers
        ];

        async function showDicomTags() {
            const record = window.uploadPreview.currentTagsRecord;
            if (!record) {
                toastr.warning("No record selected.");
                return;
            }

            try {
                const buffer = await record.file.arrayBuffer();
                const byteArray = new Uint8Array(buffer);
                const dataSet = dicomParser.parseDicom(byteArray);
                const tags = {};
                const phiTags = [];

                for (const tag in dataSet.elements) {
                    try {
                        const value = dataSet.string(tag);
                        if (value && value.trim()) {
                            tags[tag] = value.trim();
                            if (PHI_TAGS.includes(tag)) {
                                phiTags.push(tag);
                            }
                        }
                    } catch (e) {}
                }

                const tbody = document.getElementById("tags-tbody");
                if (!tbody) {
                    toastr.error("Tags modal not found.");
                    return;
                }
                tbody.innerHTML = "";

                if (Object.keys(tags).length === 0) {
                    const row = document.createElement("tr");
                    row.innerHTML = "<td colspan='2' class='text-muted'>No tags found</td>";
                    tbody.appendChild(row);
                } else {
                    for (const [tag, value] of Object.entries(tags).sort()) {
                        const isPhi = PHI_TAGS.includes(tag);
                        const row = document.createElement("tr");
                        row.className = isPhi ? "table-warning" : "";
                        row.innerHTML = `<th style="width:35%" class="${isPhi ? "fw-bold" : ""}">${tag}${isPhi ? " ⚠️" : ""}</th><td>${document.createTextNode(value).nodeValue}</td>`;
                        tbody.appendChild(row);
                    }
                }

                if (phiTags.length) {
                    document.getElementById("tags-warning-text").textContent = `Found ${phiTags.length} tag(s) with potentially patient identifiable data (highlighted in yellow).`;
                    document.getElementById("tags-warning").style.display = "";
                } else {
                    document.getElementById("tags-warning").style.display = "none";
                }

                const modal = new bootstrap.Modal(document.getElementById("dicomTagsModal"));
                modal.show();
            } catch (err) {
                console.error(err);
                toastr.error("Failed to parse DICOM tags: " + err.message);
            }
        }

        document.addEventListener("click", (e) => {
            if (e.target.closest("#tags-filter")) {
                const q = (e.target.value || "").trim().toLowerCase();
                const rows = document.querySelectorAll("#tags-tbody tr");
                rows.forEach((row) => {
                    const th = row.querySelector("th")?.textContent || "";
                    const td = row.querySelector("td")?.textContent || "";
                    row.style.display =
                        !q || th.toLowerCase().includes(q) || td.toLowerCase().includes(q)
                        ? ""
                        : "none";
                });
            }
        });

        document.addEventListener("input", (e) => {
            if (e.target.id === "tags-filter") {
                const q = (e.target.value || "").trim().toLowerCase();
                const rows = document.querySelectorAll("#tags-tbody tr");
                rows.forEach((row) => {
                    const th = row.querySelector("th")?.textContent || "";
                    const td = row.querySelector("td")?.textContent || "";
                    row.style.display =
                        !q || th.toLowerCase().includes(q) || td.toLowerCase().includes(q)
                        ? ""
                        : "none";
                });
            }
        });

        function initUploadPage() {
            if (!document.getElementById("filepicker")) {
                return;
            }

            resetOcrDebug();
            syncOcrDebugPanelVisibility();

            document.addEventListener("click", async function (e) {
                const includeSeries = e.target.closest(".include-series");
                if (includeSeries) {
                    applyIncludeToSeries(includeSeries.dataset.seriesUid, includeSeries.checked);
                    buildSelectionSummary();
                    scheduleDuplicateCheck();
                    renderGroupedPreview();
                    return;
                }

                const includeStudy = e.target.closest(".include-study");
                if (includeStudy) {
                    applyIncludeToStudy(includeStudy.dataset.studyUid, includeStudy.dataset.value === "1");
                    scheduleDuplicateCheck();
                    renderGroupedPreview();
                    return;
                }

                const viewSeriesBtn = e.target.closest(".view-series-btn");
                if (viewSeriesBtn) {
                    const records = getSeriesRecords(viewSeriesBtn.dataset.seriesUid);
                    showRecordsInViewer(records, `Series ${viewSeriesBtn.dataset.seriesUid}`);
                    return;
                }

                const viewStudyBtn = e.target.closest(".view-study-btn");
                if (viewStudyBtn) {
                    const records = getStudyRecords(viewStudyBtn.dataset.studyUid);
                    showRecordsInViewer(records, `Study ${viewStudyBtn.dataset.studyUid}`);
                    return;
                }
            });

            bindIfExists("check-anonymisation-tags", "change", () => {
                window.uploadPreview.records.forEach((record) => evaluateTagWarnings(record));
                renderGroupedPreview();
            });

            bindIfExists("check-anonymisation-ocr", "change", async () => {
                syncOcrDebugPanelVisibility();
                showLoading(true, "Updating OCR checks...");
                await runOcrChecks();
                renderGroupedPreview();
                showLoading(false);
            });

            bindIfExists("check-duplicates", "change", async () => {
                scheduleDuplicateCheck();
                renderGroupedPreview();
            });

            bindIfExists("deselect-duplicates", "click", () => {
                deselectDuplicateSeries();
            });

            bindIfExists("show-thumbnails", "change", () => {
                renderGroupedPreview();
            });

            bindIfExists("override-anon", "click", () => {
                window.uploadPreview.forceAnonymisationOverride = true;
                updateUploadButtonState();
                toastr.info("Anonymisation override enabled for this selection.");
            });

            bindIfExists("filepicker", "change", onFilesChanged);
            bindIfExists("uploadButton", "click", onUploadButtonClicked);
            bindIfExists("viewer-tags-btn", "click", showDicomTags);

            window.addEventListener("beforeunload", async () => {
                if (window.uploadPreview.ocrWorker) {
                    try { await window.uploadPreview.ocrWorker.terminate(); } catch (err) {}
                }
                window.uploadPreview.records.forEach((record) => {
                    if (record.objectUrl) URL.revokeObjectURL(record.objectUrl);
                });
            });
        }

        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", initUploadPage);
        } else {
            initUploadPage();
        }
    </script>
{% endblock %}

{% block css %}
    <style>
        .study-block {
            margin: 8px 0;
            padding: 8px;
            border: 1px solid rgba(255, 255, 255, 0.08);
            border-radius: 10px;
            background: rgba(255, 255, 255, 0.02);
        }

        .study-series-list {
            list-style: none;
            margin: 0;
            padding: 0;
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
        }

        .series-card {
            flex: 1 1 calc(33.333% - 8px);
            min-width: 280px;
            list-style: none;
            border: 1px solid rgba(255, 255, 255, 0.08);
            border-radius: 10px;
            padding: 12px;
            background: rgba(255, 255, 255, 0.01);
            display: flex;
            flex-direction: column;
            position: relative;
            z-index: 0;
        }

        .series-card.warning {
            border-color: rgba(245, 158, 11, 0.7);
            background: rgba(245, 158, 11, 0.08);
        }

        .series-card.duplicate {
            border-color: rgba(220, 53, 69, 0.8);
            background: rgba(220, 53, 69, 0.08);
        }

        .series-card.excluded {
            border-color: rgba(148, 163, 184, 0.35);
            background: rgba(148, 163, 184, 0.08);
            opacity: 0.55;
            filter: grayscale(0.25);
        }

        .series-header {
            display: flex;
            justify-content: space-between;
            gap: 10px;
            flex-wrap: wrap;
        }

        .series-actions {
            display: flex;
            flex-direction: column;
            align-items: flex-end;
            gap: 6px;
            min-width: 130px;
            flex-shrink: 0;
        }

        .thumb-preview {
            width: 140px;
            max-width: 100%;
            border-radius: 6px;
            border: 1px solid rgba(255, 255, 255, 0.1);
        }

        .dicom-viewer {
            min-height: 420px;
        }

        @media (max-width: 991px) {
            .series-card {
                flex: 1 1 calc(50% - 8px);
                min-width: 260px;
            }
        }

        @media (max-width: 575px) {
            .series-card {
                flex: 1 1 100%;
                min-width: auto;
            }
            .series-header {
                flex-direction: column;
            }
            .series-actions {
                align-items: flex-start;
            }
        }

        .dicom-viewer-root {
            background: #222;
        }

        #tags-filter {
            margin-bottom: 1rem;
        }

        .table-warning {
            background-color: rgba(245, 158, 11, 0.1) !important;
        }

        .table-warning th {
            color: #ffc107;
        }

        #ocr-debug-panel {
            border: 1px solid rgba(255, 255, 255, 0.08);
            border-radius: 8px;
            padding: 8px 10px;
            background: rgba(255, 255, 255, 0.02);
        }

        .ocr-debug-table-wrap {
            max-height: 320px;
        }

        .ocr-debug-table code {
            white-space: pre-wrap;
            word-break: break-word;
            font-size: 0.75rem;
        }
    </style>
{% endblock %}
