/**
 * Calibration — calibration modal, canvas, drag handlers, edge test.
 */

import {
    calibrationTestState, EDGE_TEST_COLORS, displaysCache,
} from '../core/state.ts';
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.ts';
import { colorStripSourcesCache, devicesCache } from '../core/state.ts';
import { t } from '../core/i18n.ts';
import { showToast } from '../core/ui.ts';
import { Modal } from '../core/modal.ts';
import { closeTutorial, startCalibrationTutorial } from './tutorials.ts';
import { startCSSOverlay, stopCSSOverlay } from './color-strips.ts';
import { ICON_WARNING, ICON_ROTATE_CW, ICON_ROTATE_CCW } from '../core/icons.ts';
import type { Calibration } from '../types.ts';

/* ── CalibrationModal subclass ────────────────────────────────── */

class CalibrationModal extends Modal {
    constructor() {
        super('calibration-modal');
    }

    snapshotValues() {
        return {
            start_position: (this.$('cal-start-position') as HTMLSelectElement).value,
            layout:         (this.$('cal-layout') as HTMLSelectElement).value,
            offset:         (this.$('cal-offset') as HTMLInputElement).value,
            top:            (this.$('cal-top-leds') as HTMLInputElement).value,
            right:          (this.$('cal-right-leds') as HTMLInputElement).value,
            bottom:         (this.$('cal-bottom-leds') as HTMLInputElement).value,
            left:           (this.$('cal-left-leds') as HTMLInputElement).value,
            spans:          JSON.stringify(window.edgeSpans),
            skip_start:     (this.$('cal-skip-start') as HTMLInputElement).value,
            skip_end:       (this.$('cal-skip-end') as HTMLInputElement).value,
            border_width:   (this.$('cal-border-width') as HTMLInputElement).value,
            led_count:      (this.$('cal-css-led-count') as HTMLInputElement).value,
        };
    }

    onForceClose() {
        closeTutorial();
        if (_isCSS()) {
            const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement)?.value;
            if (_overlayStartedHere && cssId) {
                stopCSSOverlay(cssId);
                _overlayStartedHere = false;
            }
            _clearCSSTestMode();
            (document.getElementById('calibration-css-id') as HTMLInputElement).value = '';
            const testGroup = document.getElementById('calibration-css-test-group');
            if (testGroup) testGroup.style.display = 'none';
        } else {
            const deviceId = (this.$('calibration-device-id') as HTMLInputElement).value;
            if (deviceId) clearTestMode(deviceId);
        }
        if (window._calibrationResizeObserver) window._calibrationResizeObserver.disconnect();
        const error = this.$('calibration-error');
        if (error) error.style.display = 'none';
    }
}

const calibModal = new CalibrationModal();

let _dragRaf: number | null = null;
let _previewRaf: number | null = null;
let _overlayStartedHere = false;

/* ── Helpers ──────────────────────────────────────────────────── */

function _isCSS() {
    return !!((document.getElementById('calibration-css-id') as HTMLInputElement)?.value);
}

function _cssStateKey() {
    return `css_${(document.getElementById('calibration-css-id') as HTMLInputElement).value}`;
}

async function _clearCSSTestMode() {
    const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement)?.value;
    const stateKey = _cssStateKey();
    if (!cssId || !calibrationTestState[stateKey] || calibrationTestState[stateKey].size === 0) return;
    calibrationTestState[stateKey] = new Set();
    const testDeviceId = (document.getElementById('calibration-test-device') as HTMLSelectElement)?.value;
    if (!testDeviceId) return;
    try {
        await fetchWithAuth(`/color-strip-sources/${cssId}/calibration/test`, {
            method: 'PUT',
            body: JSON.stringify({ device_id: testDeviceId, edges: {} }),
        });
    } catch (err) {
        console.error('Failed to clear CSS test mode:', err);
    }
}

function _setOverlayBtnActive(active: any) {
    const btn = document.getElementById('calibration-overlay-btn');
    if (!btn) return;
    btn.classList.toggle('active', active);
}

async function _checkOverlayStatus(cssId: any) {
    try {
        const resp = await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/status`);
        if (resp.ok) {
            const data = await resp.json();
            _setOverlayBtnActive(data.active);
        }
    } catch { /* ignore */ }
}

export async function toggleCalibrationOverlay() {
    const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement)?.value;
    if (!cssId) return;
    try {
        const resp = await fetchWithAuth(`/color-strip-sources/${cssId}/overlay/status`);
        if (!resp.ok) return;
        const { active } = await resp.json();
        if (active) {
            await stopCSSOverlay(cssId);
            _setOverlayBtnActive(false);
            _overlayStartedHere = false;
        } else {
            await startCSSOverlay(cssId);
            _setOverlayBtnActive(true);
            _overlayStartedHere = true;
        }
    } catch (err: any) {
        if (err.isAuth) return;
        console.error('Failed to toggle calibration overlay:', err);
    }
}

/* ── Public API (exported names unchanged) ────────────────────── */

export async function showCalibration(deviceId: any) {
    try {
        const [response, displays] = await Promise.all([
            fetchWithAuth(`/devices/${deviceId}`),
            displaysCache.fetch().catch((): any[] => []),
        ]);

        if (!response.ok) { showToast(t('calibration.error.load_failed'), 'error'); return; }

        const device = await response.json();
        const calibration = device.calibration;

        const preview = document.querySelector('.calibration-preview') as HTMLElement;
        const displayIndex = device.settings?.display_index ?? 0;
        const display = (displays ?? []).find((d: any) => d.index === displayIndex);
        if (display && display.width && display.height) {
            preview.style.aspectRatio = `${display.width} / ${display.height}`;
        } else {
            preview.style.aspectRatio = '';
        }

        (document.getElementById('calibration-device-id') as HTMLInputElement).value = device.id;
        (document.getElementById('cal-device-led-count-inline') as HTMLElement).textContent = device.led_count;
        (document.getElementById('cal-css-led-count-group') as HTMLElement).style.display = 'none';
        (document.getElementById('calibration-overlay-btn') as HTMLElement).style.display = 'none';

        (document.getElementById('cal-start-position') as HTMLSelectElement).value = calibration.start_position;
        (document.getElementById('cal-layout') as HTMLSelectElement).value = calibration.layout;
        (document.getElementById('cal-offset') as HTMLInputElement).value = calibration.offset || 0;

        (document.getElementById('cal-top-leds') as HTMLInputElement).value = calibration.leds_top || 0;
        (document.getElementById('cal-right-leds') as HTMLInputElement).value = calibration.leds_right || 0;
        (document.getElementById('cal-bottom-leds') as HTMLInputElement).value = calibration.leds_bottom || 0;
        (document.getElementById('cal-left-leds') as HTMLInputElement).value = calibration.leds_left || 0;

        (document.getElementById('cal-skip-start') as HTMLInputElement).value = calibration.skip_leds_start || 0;
        (document.getElementById('cal-skip-end') as HTMLInputElement).value = calibration.skip_leds_end || 0;
        updateOffsetSkipLock();

        (document.getElementById('cal-border-width') as HTMLInputElement).value = calibration.border_width || 10;

        window.edgeSpans = {
            top: { start: calibration.span_top_start ?? 0, end: calibration.span_top_end ?? 1 },
            right: { start: calibration.span_right_start ?? 0, end: calibration.span_right_end ?? 1 },
            bottom: { start: calibration.span_bottom_start ?? 0, end: calibration.span_bottom_end ?? 1 },
            left: { start: calibration.span_left_start ?? 0, end: calibration.span_left_end ?? 1 },
        };

        calibrationTestState[device.id] = new Set();

        updateCalibrationPreview();

        calibModal.snapshot();
        calibModal.open();

        initSpanDrag();
        requestAnimationFrame(() => {
            renderCalibrationCanvas();
            if (!localStorage.getItem('calibrationTutorialSeen')) {
                localStorage.setItem('calibrationTutorialSeen', '1');
                startCalibrationTutorial();
            }
        });

        if (!window._calibrationResizeObserver) {
            window._calibrationResizeObserver = new ResizeObserver(() => {
                if (window._calibrationResizeRaf) return;
                window._calibrationResizeRaf = requestAnimationFrame(() => {
                    window._calibrationResizeRaf = null;
                    updateSpanBars();
                    renderCalibrationCanvas();
                });
            });
        }
        window._calibrationResizeObserver.observe(preview);

    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to load calibration:', error);
        showToast(t('calibration.error.load_failed'), 'error');
    }
}

function isCalibrationDirty() {
    return calibModal.isDirty();
}

export function forceCloseCalibrationModal() {
    calibModal.forceClose();
}

export async function closeCalibrationModal() {
    calibModal.close();
}

/* ── CSS Calibration support ──────────────────────────────────── */

export async function showCSSCalibration(cssId: any) {
    try {
        const [cssSources, devices] = await Promise.all([
            colorStripSourcesCache.fetch(),
            devicesCache.fetch().catch((): any[] => []),
        ]);
        const source = cssSources.find((s: any) => s.id === cssId);
        if (!source) { showToast(t('calibration.error.css_load_failed'), 'error'); return; }
        const calibration: Calibration = source.calibration || {} as Calibration;

        // Set CSS mode — clear device-id, set css-id
        (document.getElementById('calibration-device-id') as HTMLInputElement).value = '';
        (document.getElementById('calibration-css-id') as HTMLInputElement).value = cssId;

        // Populate device picker for edge test
        const testDeviceSelect = document.getElementById('calibration-test-device') as HTMLSelectElement;
        testDeviceSelect.innerHTML = '';
        devices.forEach((d: any) => {
            const opt = document.createElement('option');
            opt.value = d.id;
            opt.textContent = d.name;
            testDeviceSelect.appendChild(opt);
        });
        const testGroup = document.getElementById('calibration-css-test-group') as HTMLElement;
        testGroup.style.display = devices.length ? '' : 'none';

        // Pre-select device: 1) LED count match, 2) last remembered, 3) first
        if (devices.length) {
            const rememberedId = localStorage.getItem('css_calibration_test_device');
            let selected: any = null;
            if (source.led_count > 0) {
                selected = devices.find((d: any) => d.led_count === source.led_count) || null;
            }
            if (!selected && rememberedId) {
                selected = devices.find((d: any) => d.id === rememberedId) || null;
            }
            if (selected) testDeviceSelect.value = selected.id;
            testDeviceSelect.onchange = () => localStorage.setItem('css_calibration_test_device', testDeviceSelect.value);
        }

        // Populate calibration fields
        const preview = document.querySelector('.calibration-preview') as HTMLElement;
        preview.style.aspectRatio = '';
        (document.getElementById('cal-device-led-count-inline') as HTMLElement).textContent = '—';
        const ledCountGroup = document.getElementById('cal-css-led-count-group') as HTMLElement;
        ledCountGroup.style.display = '';
        const calLeds = (calibration.leds_top || 0) + (calibration.leds_right || 0) +
                        (calibration.leds_bottom || 0) + (calibration.leds_left || 0);
        (document.getElementById('cal-css-led-count') as HTMLInputElement).value = String(source.led_count || calLeds || 0);

        (document.getElementById('cal-start-position') as HTMLSelectElement).value = calibration.start_position || 'bottom_left';
        (document.getElementById('cal-layout') as HTMLSelectElement).value = calibration.layout || 'clockwise';
        (document.getElementById('cal-offset') as HTMLInputElement).value = String(calibration.offset || 0);

        (document.getElementById('cal-top-leds') as HTMLInputElement).value = String(calibration.leds_top || 0);
        (document.getElementById('cal-right-leds') as HTMLInputElement).value = String(calibration.leds_right || 0);
        (document.getElementById('cal-bottom-leds') as HTMLInputElement).value = String(calibration.leds_bottom || 0);
        (document.getElementById('cal-left-leds') as HTMLInputElement).value = String(calibration.leds_left || 0);

        (document.getElementById('cal-skip-start') as HTMLInputElement).value = String(calibration.skip_leds_start || 0);
        (document.getElementById('cal-skip-end') as HTMLInputElement).value = String(calibration.skip_leds_end || 0);
        updateOffsetSkipLock();

        (document.getElementById('cal-border-width') as HTMLInputElement).value = String(calibration.border_width || 10);

        window.edgeSpans = {
            top: { start: calibration.span_top_start ?? 0, end: calibration.span_top_end ?? 1 },
            right: { start: calibration.span_right_start ?? 0, end: calibration.span_right_end ?? 1 },
            bottom: { start: calibration.span_bottom_start ?? 0, end: calibration.span_bottom_end ?? 1 },
            left: { start: calibration.span_left_start ?? 0, end: calibration.span_left_end ?? 1 },
        };

        calibrationTestState[_cssStateKey()] = new Set();

        updateCalibrationPreview();

        calibModal.snapshot();
        calibModal.open();

        // Show overlay toggle and check current status
        _overlayStartedHere = false;
        const overlayBtn = document.getElementById('calibration-overlay-btn') as HTMLElement;
        overlayBtn.style.display = '';
        _setOverlayBtnActive(false);
        _checkOverlayStatus(cssId);

        initSpanDrag();
        requestAnimationFrame(() => renderCalibrationCanvas());

        if (!window._calibrationResizeObserver) {
            window._calibrationResizeObserver = new ResizeObserver(() => {
                if (window._calibrationResizeRaf) return;
                window._calibrationResizeRaf = requestAnimationFrame(() => {
                    window._calibrationResizeRaf = null;
                    updateSpanBars();
                    renderCalibrationCanvas();
                });
            });
        }
        window._calibrationResizeObserver.observe(preview);

    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to load CSS calibration:', error);
        showToast(t('calibration.error.load_failed'), 'error');
    }
}

export function updateOffsetSkipLock() {
    const offsetEl = document.getElementById('cal-offset') as HTMLInputElement;
    const skipStartEl = document.getElementById('cal-skip-start') as HTMLInputElement;
    const skipEndEl = document.getElementById('cal-skip-end') as HTMLInputElement;
    const hasOffset = parseInt(offsetEl.value || '0') > 0;
    const hasSkip = parseInt(skipStartEl.value || '0') > 0 || parseInt(skipEndEl.value || '0') > 0;
    skipStartEl.disabled = hasOffset;
    skipEndEl.disabled = hasOffset;
    offsetEl.disabled = hasSkip;
}

export function updateCalibrationPreview() {
    const total = parseInt((document.getElementById('cal-top-leds') as HTMLInputElement).value || '0') +
                  parseInt((document.getElementById('cal-right-leds') as HTMLInputElement).value || '0') +
                  parseInt((document.getElementById('cal-bottom-leds') as HTMLInputElement).value || '0') +
                  parseInt((document.getElementById('cal-left-leds') as HTMLInputElement).value || '0');
    const totalEl = document.querySelector('.preview-screen-total') as HTMLElement;
    const inCSS = _isCSS();
    const declaredCount = inCSS
        ? parseInt((document.getElementById('cal-css-led-count') as HTMLInputElement).value || '0')
        : parseInt((document.getElementById('cal-device-led-count-inline') as HTMLElement).textContent || '0');
    if (inCSS) {
        (document.getElementById('cal-device-led-count-inline') as HTMLElement).textContent = String(declaredCount || '—');
    }
    // In device mode: calibration total must exactly equal device LED count
    // In CSS mode: warn only if calibrated LEDs exceed the declared total (padding handles the rest)
    const mismatch = inCSS
        ? (declaredCount > 0 && total > declaredCount)
        : (total !== declaredCount);
    (document.getElementById('cal-total-leds-inline') as HTMLElement).innerHTML = (mismatch ? ICON_WARNING + ' ' : '') + total;
    if (totalEl) totalEl.classList.toggle('mismatch', mismatch);

    const startPos = (document.getElementById('cal-start-position') as HTMLSelectElement).value;
    ['top_left', 'top_right', 'bottom_left', 'bottom_right'].forEach(corner => {
        const cornerEl = document.querySelector(`.preview-corner.corner-${corner.replace('_', '-')}`) as HTMLElement;
        if (cornerEl) {
            if (corner === startPos) cornerEl.classList.add('active');
            else cornerEl.classList.remove('active');
        }
    });

    const direction = (document.getElementById('cal-layout') as HTMLSelectElement).value;
    const dirIcon = document.getElementById('direction-icon');
    const dirLabel = document.getElementById('direction-label');
    if (dirIcon) dirIcon.innerHTML = direction === 'clockwise' ? ICON_ROTATE_CW : ICON_ROTATE_CCW;
    if (dirLabel) dirLabel.textContent = direction === 'clockwise' ? 'CW' : 'CCW';

    const deviceId = (document.getElementById('calibration-device-id') as HTMLInputElement).value;
    const stateKey = _isCSS() ? _cssStateKey() : deviceId;
    const activeEdges = calibrationTestState[stateKey] || new Set();

    ['top', 'right', 'bottom', 'left'].forEach(edge => {
        const toggleEl = document.querySelector(`.edge-toggle.toggle-${edge}`) as HTMLElement;
        if (!toggleEl) return;
        if (activeEdges.has(edge)) {
            const [r, g, b] = EDGE_TEST_COLORS[edge];
            toggleEl.style.background = `rgba(${r}, ${g}, ${b}, 0.35)`;
            toggleEl.style.boxShadow = `inset 0 0 6px rgba(${r}, ${g}, ${b}, 0.5)`;
        } else {
            toggleEl.style.background = '';
            toggleEl.style.boxShadow = '';
        }
    });

    ['top', 'right', 'bottom', 'left'].forEach(edge => {
        const count = parseInt((document.getElementById(`cal-${edge}-leds`) as HTMLInputElement).value) || 0;
        const edgeEl = document.querySelector(`.preview-edge.edge-${edge}`) as HTMLElement;
        const toggleEl = document.querySelector(`.edge-toggle.toggle-${edge}`) as HTMLElement;
        if (edgeEl) edgeEl.classList.toggle('edge-disabled', count === 0);
        if (toggleEl) toggleEl.classList.toggle('edge-disabled', count === 0);
    });

    if (_previewRaf) cancelAnimationFrame(_previewRaf);
    _previewRaf = requestAnimationFrame(() => {
        _previewRaf = null;
        updateSpanBars();
        renderCalibrationCanvas();
    });
}

export function renderCalibrationCanvas() {
    const canvas = document.getElementById('calibration-preview-canvas') as HTMLCanvasElement;
    if (!canvas) return;

    const container = canvas.parentElement;
    const containerRect = container!.getBoundingClientRect();
    if (containerRect.width === 0 || containerRect.height === 0) return;

    const padX = 40;
    const padY = 40;

    const dpr = window.devicePixelRatio || 1;
    const canvasW = containerRect.width + padX * 2;
    const canvasH = containerRect.height + padY * 2;
    canvas.width = canvasW * dpr;
    canvas.height = canvasH * dpr;
    const ctx = canvas.getContext('2d')!;
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, canvasW, canvasH);

    const ox = padX;
    const oy = padY;
    const cW = containerRect.width;
    const cH = containerRect.height;

    const startPos = (document.getElementById('cal-start-position') as HTMLSelectElement).value;
    const layout = (document.getElementById('cal-layout') as HTMLSelectElement).value;
    const offset = parseInt((document.getElementById('cal-offset') as HTMLInputElement).value || '0');
    const calibration = {
        start_position: startPos,
        layout: layout,
        offset: offset,
        leds_top: parseInt((document.getElementById('cal-top-leds') as HTMLInputElement).value || '0'),
        leds_right: parseInt((document.getElementById('cal-right-leds') as HTMLInputElement).value || '0'),
        leds_bottom: parseInt((document.getElementById('cal-bottom-leds') as HTMLInputElement).value || '0'),
        leds_left: parseInt((document.getElementById('cal-left-leds') as HTMLInputElement).value || '0'),
    };
    const skipStart = parseInt((document.getElementById('cal-skip-start') as HTMLInputElement).value || '0');
    const skipEnd = parseInt((document.getElementById('cal-skip-end') as HTMLInputElement).value || '0');

    const segments = buildSegments(calibration);
    if (segments.length === 0) return;

    const totalLeds = calibration.leds_top + calibration.leds_right + calibration.leds_bottom + calibration.leds_left;
    const hasSkip = (skipStart > 0 || skipEnd > 0) && totalLeds > 1;

    const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
    const tickStroke = isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.3)';
    const tickFill = isDark ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.6)';
    const chevronStroke = isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.4)';

    const cw = 56;
    const ch = 36;

    const spans = window.edgeSpans || {};
    const edgeLenH = cW - 2 * cw;
    const edgeLenV = cH - 2 * ch;

    const edgeGeometry: any = {
        top:    { x1: ox + cw + (spans.top?.start || 0) * edgeLenH, x2: ox + cw + (spans.top?.end || 1) * edgeLenH, midY: oy + ch / 2, horizontal: true },
        bottom: { x1: ox + cw + (spans.bottom?.start || 0) * edgeLenH, x2: ox + cw + (spans.bottom?.end || 1) * edgeLenH, midY: oy + cH - ch / 2, horizontal: true },
        left:   { y1: oy + ch + (spans.left?.start || 0) * edgeLenV, y2: oy + ch + (spans.left?.end || 1) * edgeLenV, midX: ox + cw / 2, horizontal: false },
        right:  { y1: oy + ch + (spans.right?.start || 0) * edgeLenV, y2: oy + ch + (spans.right?.end || 1) * edgeLenV, midX: ox + cW - cw / 2, horizontal: false },
    };

    const toggleSize = 16;
    const axisPos: any = {
        top:    oy - toggleSize - 3,
        bottom: oy + cH + toggleSize + 3,
        left:   ox - toggleSize - 3,
        right:  ox + cW + toggleSize + 3,
    };

    const arrowInset = 12;
    const arrowPos: any = {
        top:    oy + ch + arrowInset,
        bottom: oy + cH - ch - arrowInset,
        left:   ox + cw + arrowInset,
        right:  ox + cW - cw - arrowInset,
    };

    segments.forEach((seg: any) => {
        const geo = edgeGeometry[seg.edge];
        if (!geo) return;

        const count = seg.led_count;
        if (count === 0) return;

        const edgeDisplayStart = hasSkip ? Math.max(seg.led_start, skipStart) : seg.led_start;
        const edgeDisplayEnd = hasSkip ? Math.min(seg.led_start + count, totalLeds - skipEnd) : seg.led_start + count - 1;
        const edgeDisplayRange = edgeDisplayEnd - edgeDisplayStart;
        const toEdgeLabel = (i: number) => {
            if (!hasSkip) return totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
            if (count <= 1) return edgeDisplayStart;
            return Math.round(edgeDisplayStart + i / (count - 1) * edgeDisplayRange);
        };

        const edgeBounds = new Set<number>();
        edgeBounds.add(0);
        if (count > 1) edgeBounds.add(count - 1);

        const specialTicks = new Set<number>();
        if (offset > 0 && totalLeds > 0) {
            const zeroPos = (totalLeds - seg.led_start % totalLeds) % totalLeds;
            if (zeroPos < count) specialTicks.add(zeroPos);
        }

        const labelsToShow = new Set<number>([...specialTicks]);

        if (count > 2) {
            const edgeLen = geo.horizontal ? (geo.x2 - geo.x1) : (geo.y2 - geo.y1);
            const maxDigits = String(totalLeds > 0 ? totalLeds - 1 : count - 1).length;
            const minSpacing = geo.horizontal ? maxDigits * 7 + 8 : 22;

            const allMandatory = new Set([...edgeBounds, ...specialTicks]);
            const maxIntermediate = Math.max(0, 5 - allMandatory.size);
            const niceSteps = [5, 10, 25, 50, 100, 250, 500];
            let step = niceSteps[niceSteps.length - 1];
            for (const s of niceSteps) {
                if (Math.floor(count / s) <= maxIntermediate) { step = s; break; }
            }

            const tickPx = (i: number) => {
                const f = i / (count - 1);
                return (seg.reverse ? (1 - f) : f) * edgeLen;
            };

            const placed: number[] = [];
            specialTicks.forEach(i => placed.push(tickPx(i)));

            for (let i = 1; i < count - 1; i++) {
                if (specialTicks.has(i)) continue;
                if (toEdgeLabel(i) % step === 0) {
                    const px = tickPx(i);
                    if (!placed.some(p => Math.abs(px - p) < minSpacing)) {
                        labelsToShow.add(i);
                        placed.push(px);
                    }
                }
            }

            edgeBounds.forEach(bi => {
                if (labelsToShow.has(bi) || specialTicks.has(bi)) return;
                labelsToShow.add(bi);
                placed.push(tickPx(bi));
            });
        } else {
            edgeBounds.forEach(i => labelsToShow.add(i));
        }

        const tickLenLong = toggleSize + 3;
        const tickLenShort = 4;
        ctx.strokeStyle = tickStroke;
        ctx.lineWidth = 1;
        ctx.fillStyle = tickFill;
        ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';

        labelsToShow.forEach(i => {
            const fraction = count > 1 ? i / (count - 1) : 0.5;
            const displayFraction = seg.reverse ? (1 - fraction) : fraction;
            const displayLabel = toEdgeLabel(i);
            const tickLen = edgeBounds.has(i) ? tickLenLong : tickLenShort;

            if (geo.horizontal) {
                const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
                const axisY = axisPos[seg.edge];
                const tickDir = seg.edge === 'top' ? 1 : -1;
                ctx.beginPath(); ctx.moveTo(tx, axisY); ctx.lineTo(tx, axisY + tickDir * tickLen); ctx.stroke();
                ctx.textAlign = 'center';
                ctx.textBaseline = seg.edge === 'top' ? 'bottom' : 'top';
                ctx.fillText(String(displayLabel), tx, axisY - tickDir * 1);
            } else {
                const ty = geo.y1 + displayFraction * (geo.y2 - geo.y1);
                const axisX = axisPos[seg.edge];
                const tickDir = seg.edge === 'left' ? 1 : -1;
                ctx.beginPath(); ctx.moveTo(axisX, ty); ctx.lineTo(axisX + tickDir * tickLen, ty); ctx.stroke();
                ctx.textBaseline = 'middle';
                ctx.textAlign = seg.edge === 'left' ? 'right' : 'left';
                ctx.fillText(String(displayLabel), axisX - tickDir * 1, ty);
            }
        });

        const s = 7;
        let mx, my, angle;
        if (geo.horizontal) {
            mx = ox + cw + edgeLenH / 2;
            my = arrowPos[seg.edge];
            angle = seg.reverse ? Math.PI : 0;
        } else {
            mx = arrowPos[seg.edge];
            my = oy + ch + edgeLenV / 2;
            angle = seg.reverse ? -Math.PI / 2 : Math.PI / 2;
        }

        ctx.save();
        ctx.translate(mx, my);
        ctx.rotate(angle);
        ctx.fillStyle = 'rgba(76, 175, 80, 0.85)';
        ctx.strokeStyle = chevronStroke;
        ctx.lineWidth = 1;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.beginPath();
        ctx.moveTo(-s * 0.5, -s * 0.6);
        ctx.lineTo(s * 0.5, 0);
        ctx.lineTo(-s * 0.5, s * 0.6);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.restore();
    });
}

function updateSpanBars() {
    const spans = window.edgeSpans || {};
    const container = document.querySelector('.calibration-preview') as HTMLElement;
    ['top', 'right', 'bottom', 'left'].forEach(edge => {
        const bar = document.querySelector(`.edge-span-bar[data-edge="${edge}"]`) as HTMLElement;
        if (!bar) return;
        const span = spans[edge] || { start: 0, end: 1 };
        const edgeEl = bar.parentElement as HTMLElement;
        const isHorizontal = (edge === 'top' || edge === 'bottom');

        if (isHorizontal) {
            const totalWidth = edgeEl.clientWidth;
            bar.style.left = (span.start * totalWidth) + 'px';
            bar.style.width = ((span.end - span.start) * totalWidth) + 'px';
        } else {
            const totalHeight = edgeEl.clientHeight;
            bar.style.top = (span.start * totalHeight) + 'px';
            bar.style.height = ((span.end - span.start) * totalHeight) + 'px';
        }

        if (!container) return;
        const toggle = container.querySelector(`.toggle-${edge}`) as HTMLElement;
        if (!toggle) return;
        if (isHorizontal) {
            const cornerW = 56;
            const edgeW = container.clientWidth - 2 * cornerW;
            toggle.style.left = (cornerW + span.start * edgeW) + 'px';
            toggle.style.right = 'auto';
            toggle.style.width = ((span.end - span.start) * edgeW) + 'px';
        } else {
            const cornerH = 36;
            const edgeH = container.clientHeight - 2 * cornerH;
            toggle.style.top = (cornerH + span.start * edgeH) + 'px';
            toggle.style.bottom = 'auto';
            toggle.style.height = ((span.end - span.start) * edgeH) + 'px';
        }
    });
}

function initSpanDrag() {
    const MIN_SPAN = 0.05;

    document.querySelectorAll('.edge-span-bar').forEach(bar => {
        const edge = (bar as HTMLElement).dataset.edge!;
        const isHorizontal = (edge === 'top' || edge === 'bottom');

        bar.addEventListener('click', (e: Event) => e.stopPropagation());

        bar.querySelectorAll('.edge-span-handle').forEach(handle => {
            handle.addEventListener('mousedown', (e: Event) => {
                const edgeLeds = parseInt((document.getElementById(`cal-${edge}-leds`) as HTMLInputElement).value) || 0;
                if (edgeLeds === 0) return;
                e.preventDefault();
                e.stopPropagation();
                const handleType = (handle as HTMLElement).dataset.handle;
                const edgeEl = bar.parentElement as HTMLElement;
                const rect = edgeEl.getBoundingClientRect();

                function onMouseMove(ev: MouseEvent) {
                    const span = window.edgeSpans[edge];
                    let fraction;
                    if (isHorizontal) fraction = (ev.clientX - rect.left) / rect.width;
                    else fraction = (ev.clientY - rect.top) / rect.height;
                    fraction = Math.max(0, Math.min(1, fraction));

                    if (handleType === 'start') span.start = Math.min(fraction, span.end - MIN_SPAN);
                    else span.end = Math.max(fraction, span.start + MIN_SPAN);

                    if (!_dragRaf) {
                        _dragRaf = requestAnimationFrame(() => {
                            _dragRaf = null;
                            updateSpanBars();
                            renderCalibrationCanvas();
                        });
                    }
                }

                function onMouseUp() {
                    if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; }
                    updateSpanBars();
                    renderCalibrationCanvas();
                    document.removeEventListener('mousemove', onMouseMove);
                    document.removeEventListener('mouseup', onMouseUp);
                }

                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            });
        });

        bar.addEventListener('mousedown', (e: Event) => {
            if ((e.target as HTMLElement).classList.contains('edge-span-handle')) return;
            e.preventDefault();
            e.stopPropagation();

            const edgeEl = bar.parentElement as HTMLElement;
            const rect = edgeEl.getBoundingClientRect();
            const span = window.edgeSpans[edge];
            const spanWidth = span.end - span.start;

            let startFraction;
            if (isHorizontal) startFraction = ((e as MouseEvent).clientX - rect.left) / rect.width;
            else startFraction = ((e as MouseEvent).clientY - rect.top) / rect.height;
            const offsetInSpan = startFraction - span.start;

            function onMouseMove(ev: MouseEvent) {
                let fraction;
                if (isHorizontal) fraction = (ev.clientX - rect.left) / rect.width;
                else fraction = (ev.clientY - rect.top) / rect.height;

                let newStart = fraction - offsetInSpan;
                newStart = Math.max(0, Math.min(1 - spanWidth, newStart));
                span.start = newStart;
                span.end = newStart + spanWidth;

                if (!_dragRaf) {
                    _dragRaf = requestAnimationFrame(() => {
                        _dragRaf = null;
                        updateSpanBars();
                        renderCalibrationCanvas();
                    });
                }
            }

            function onMouseUp() {
                if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; }
                updateSpanBars();
                renderCalibrationCanvas();
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            }

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    });

    updateSpanBars();
}

export function setStartPosition(position: any) {
    (document.getElementById('cal-start-position') as HTMLSelectElement).value = position;
    updateCalibrationPreview();
}

export function toggleEdgeInputs() {
    const preview = document.querySelector('.calibration-preview');
    if (preview) preview.classList.toggle('inputs-dimmed');
}

export function toggleDirection() {
    const select = document.getElementById('cal-layout') as HTMLSelectElement;
    select.value = select.value === 'clockwise' ? 'counterclockwise' : 'clockwise';
    updateCalibrationPreview();
}

export async function toggleTestEdge(edge: any) {
    const edgeLeds = parseInt((document.getElementById(`cal-${edge}-leds`) as HTMLInputElement).value) || 0;
    if (edgeLeds === 0) return;

    const error = document.getElementById('calibration-error') as HTMLElement;

    if (_isCSS()) {
        const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement).value;
        const testDeviceId = (document.getElementById('calibration-test-device') as HTMLSelectElement)?.value;
        if (!testDeviceId) return;

        const stateKey = _cssStateKey();
        if (!calibrationTestState[stateKey]) calibrationTestState[stateKey] = new Set();
        if (calibrationTestState[stateKey].has(edge)) calibrationTestState[stateKey].delete(edge);
        else calibrationTestState[stateKey].add(edge);

        const edges: any = {};
        calibrationTestState[stateKey].forEach((e: any) => { edges[e] = EDGE_TEST_COLORS[e]; });
        updateCalibrationPreview();

        try {
            const response = await fetchWithAuth(`/color-strip-sources/${cssId}/calibration/test`, {
                method: 'PUT',
                body: JSON.stringify({ device_id: testDeviceId, edges }),
            });
            if (!response.ok) {
                const errorData = await response.json();
                const detail = errorData.detail || errorData.message || '';
                const detailStr = Array.isArray(detail) ? detail.map((d: any) => d.msg || d).join('; ') : String(detail);
                error.textContent = detailStr || t('calibration.error.test_toggle_failed');
                error.style.display = 'block';
            }
        } catch (err: any) {
            if (err.isAuth) return;
            console.error('Failed to toggle CSS test edge:', err);
            error.textContent = err.message || t('calibration.error.test_toggle_failed');
            error.style.display = 'block';
        }
        return;
    }

    const deviceId = (document.getElementById('calibration-device-id') as HTMLInputElement).value;
    if (!calibrationTestState[deviceId]) calibrationTestState[deviceId] = new Set();

    if (calibrationTestState[deviceId].has(edge)) calibrationTestState[deviceId].delete(edge);
    else calibrationTestState[deviceId].add(edge);

    const edges: any = {};
    calibrationTestState[deviceId].forEach((e: any) => { edges[e] = EDGE_TEST_COLORS[e]; });

    updateCalibrationPreview();

    try {
        const response = await fetchWithAuth(`/devices/${deviceId}/calibration/test`, {
            method: 'PUT',
            body: JSON.stringify({ edges })
        });
        if (!response.ok) {
            const errorData = await response.json();
            const detail = errorData.detail || errorData.message || '';
            const detailStr = Array.isArray(detail) ? detail.map((d: any) => d.msg || d).join('; ') : String(detail);
            error.textContent = detailStr || t('calibration.error.test_toggle_failed');
            error.style.display = 'block';
        }
    } catch (err: any) {
        if (err.isAuth) return;
        console.error('Failed to toggle test edge:', err);
        error.textContent = err.message || t('calibration.error.test_toggle_failed');
        error.style.display = 'block';
    }
}

async function clearTestMode(deviceId: any) {
    if (!calibrationTestState[deviceId] || calibrationTestState[deviceId].size === 0) return;
    calibrationTestState[deviceId] = new Set();
    try {
        await fetch(`${API_BASE}/devices/${deviceId}/calibration/test`, {
            method: 'PUT',
            headers: getHeaders(),
            body: JSON.stringify({ edges: {} })
        });
    } catch (err) {
        console.error('Failed to clear test mode:', err);
    }
}

export async function saveCalibration() {
    const cssMode = _isCSS();
    const deviceId = (document.getElementById('calibration-device-id') as HTMLInputElement).value;
    const cssId = (document.getElementById('calibration-css-id') as HTMLInputElement).value;
    const error = document.getElementById('calibration-error') as HTMLElement;

    if (cssMode) {
        await _clearCSSTestMode();
    } else {
        await clearTestMode(deviceId);
    }
    updateCalibrationPreview();

    const topLeds = parseInt((document.getElementById('cal-top-leds') as HTMLInputElement).value || '0');
    const rightLeds = parseInt((document.getElementById('cal-right-leds') as HTMLInputElement).value || '0');
    const bottomLeds = parseInt((document.getElementById('cal-bottom-leds') as HTMLInputElement).value || '0');
    const leftLeds = parseInt((document.getElementById('cal-left-leds') as HTMLInputElement).value || '0');
    const total = topLeds + rightLeds + bottomLeds + leftLeds;

    const declaredLedCount = cssMode
        ? parseInt((document.getElementById('cal-css-led-count') as HTMLInputElement).value) || 0
        : parseInt((document.getElementById('cal-device-led-count-inline') as HTMLElement).textContent!) || 0;
    if (!cssMode) {
        if (total !== declaredLedCount) {
            error.textContent = t('calibration.error.led_count_mismatch');
            error.style.display = 'block';
            return;
        }
    } else {
        if (declaredLedCount > 0 && total > declaredLedCount) {
            error.textContent = t('calibration.error.led_count_exceeded');
            error.style.display = 'block';
            return;
        }
    }

    const startPosition = (document.getElementById('cal-start-position') as HTMLSelectElement).value;
    const layout = (document.getElementById('cal-layout') as HTMLSelectElement).value;
    const offset = parseInt((document.getElementById('cal-offset') as HTMLInputElement).value || '0');

    const spans = window.edgeSpans || {};
    const calibration = {
        mode: 'simple',
        layout, start_position: startPosition, offset,
        leds_top: topLeds, leds_right: rightLeds, leds_bottom: bottomLeds, leds_left: leftLeds,
        span_top_start: spans.top?.start ?? 0, span_top_end: spans.top?.end ?? 1,
        span_right_start: spans.right?.start ?? 0, span_right_end: spans.right?.end ?? 1,
        span_bottom_start: spans.bottom?.start ?? 0, span_bottom_end: spans.bottom?.end ?? 1,
        span_left_start: spans.left?.start ?? 0, span_left_end: spans.left?.end ?? 1,
        skip_leds_start: parseInt((document.getElementById('cal-skip-start') as HTMLInputElement).value || '0'),
        skip_leds_end: parseInt((document.getElementById('cal-skip-end') as HTMLInputElement).value || '0'),
        border_width: parseInt((document.getElementById('cal-border-width') as HTMLInputElement).value) || 10,
    };

    try {
        let response;
        if (cssMode) {
            response = await fetchWithAuth(`/color-strip-sources/${cssId}`, {
                method: 'PUT',
                body: JSON.stringify({ calibration, led_count: declaredLedCount }),
            });
        } else {
            response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {
                method: 'PUT',
                body: JSON.stringify(calibration),
            });
        }
        if (response.ok) {
            showToast(t('calibration.saved'), 'success');
            if (cssMode) colorStripSourcesCache.invalidate();
            calibModal.forceClose();
            if (cssMode) {
                if (window.loadTargetsTab) window.loadTargetsTab();
            } else {
                window.loadDevices();
            }
        } else {
            const errorData = await response.json();
            const detail = errorData.detail || errorData.message || '';
            const detailStr = Array.isArray(detail) ? detail.map((d: any) => d.msg || d).join('; ') : String(detail);
            error.textContent = detailStr || t('calibration.error.save_failed');
            error.style.display = 'block';
        }
    } catch (err: any) {
        if (err.isAuth) return;
        console.error('Failed to save calibration:', err);
        error.textContent = err.message || t('calibration.error.save_failed');
        error.style.display = 'block';
    }
}

function getEdgeOrder(startPosition: any, layout: any) {
    const orders: any = {
        'bottom_left_clockwise': ['left', 'top', 'right', 'bottom'],
        'bottom_left_counterclockwise': ['bottom', 'right', 'top', 'left'],
        'bottom_right_clockwise': ['bottom', 'left', 'top', 'right'],
        'bottom_right_counterclockwise': ['right', 'top', 'left', 'bottom'],
        'top_left_clockwise': ['top', 'right', 'bottom', 'left'],
        'top_left_counterclockwise': ['left', 'bottom', 'right', 'top'],
        'top_right_clockwise': ['right', 'bottom', 'left', 'top'],
        'top_right_counterclockwise': ['top', 'left', 'bottom', 'right']
    };
    return orders[`${startPosition}_${layout}`] || ['left', 'top', 'right', 'bottom'];
}

function shouldReverse(edge: any, startPosition: any, layout: any) {
    const reverseRules: any = {
        'bottom_left_clockwise': { left: true, top: false, right: false, bottom: true },
        'bottom_left_counterclockwise': { bottom: false, right: true, top: true, left: false },
        'bottom_right_clockwise': { bottom: true, left: true, top: false, right: false },
        'bottom_right_counterclockwise': { right: true, top: true, left: false, bottom: false },
        'top_left_clockwise': { top: false, right: false, bottom: true, left: true },
        'top_left_counterclockwise': { left: false, bottom: false, right: true, top: true },
        'top_right_clockwise': { right: false, bottom: true, left: true, top: false },
        'top_right_counterclockwise': { top: true, left: false, bottom: false, right: true }
    };
    const rules = reverseRules[`${startPosition}_${layout}`];
    return rules ? rules[edge] : false;
}

function buildSegments(calibration: any) {
    const edgeOrder = getEdgeOrder(calibration.start_position, calibration.layout);
    const edgeCounts: any = {
        top: calibration.leds_top || 0,
        right: calibration.leds_right || 0,
        bottom: calibration.leds_bottom || 0,
        left: calibration.leds_left || 0
    };

    const segments: any[] = [];
    let ledStart = calibration.offset || 0;

    edgeOrder.forEach((edge: any) => {
        const count = edgeCounts[edge];
        if (count > 0) {
            segments.push({
                edge,
                led_start: ledStart,
                led_count: count,
                reverse: shouldReverse(edge, calibration.start_position, calibration.layout)
            });
            ledStart += count;
        }
    });

    return segments;
}
