(function () {
    const state = {
        satellites: [],
        selectedSatellite: null,
        flightLine: [],
        picking: false,
        result: null,
        viewer: null,
        mapDataSource: null,
        pickHandler: null,
        resizeObserver: null,
        cursorCoordsPanel: null,
        targetOverlay: null,
    };

    function el(id) {
        return document.getElementById(id);
    }

    function text(value, fallback = '') {
        if (value === null || value === undefined || value === '') return fallback;
        return String(value);
    }

    function escapeHtml(value) {
        return text(value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    function showAlert(message, type = 'danger') {
        el('angular-motion-alert').innerHTML = message
            ? `<div class="alert alert-${type} mb-3" role="alert">${escapeHtml(message)}</div>`
            : '';
    }

    async function requestJson(url, options = {}) {
        const response = await fetch(url, options);
        const isJson = response.headers.get('content-type')?.includes('application/json');
        const body = isJson ? await response.json() : await response.text();
        if (!response.ok) {
            if (body && typeof body === 'object') {
                throw new Error(body.error || body.message || Object.values(body).join('; '));
            }
            throw new Error(body || `HTTP ${response.status}`);
        }
        return body;
    }

    function nowLocalInput() {
        const d = new Date();
        d.setSeconds(0, 0);
        return PcpUtcTime.toLocalInputValue(d);
    }

    function localDateTimeValue() {
        return PcpUtcTime.toUtcIsoFromInput(el('angular-motion-time').value);
    }

    function toDateTimeInputValue(value) {
        return PcpUtcTime.toLocalInputValue(value);
    }

    function dateTimeMillis(value) {
        const date = PcpUtcTime.parseApiDate(value);
        return date ? date.getTime() : NaN;
    }

    function formatDateTime(value) {
        return PcpUtcTime.formatDateTime(value);
    }

    function formatNumber(value, digits = 6) {
        const number = Number(value);
        return Number.isFinite(number) ? number.toFixed(digits) : '-';
    }

    function setLoading(isLoading) {
        el('angular-motion-submit').disabled = isLoading;
        el('angular-motion-submit').textContent = isLoading ? 'Расчет...' : 'Рассчитать';
    }

    async function loadSatellites() {
        const time = localDateTimeValue();
        const query = time ? `?time=${encodeURIComponent(time)}` : '';
        state.satellites = await requestJson(`/api/angular-motion/satellites${query}`);
        renderSatellites();
        await loadFlightLine();
    }

    function renderSatellites() {
        const select = el('angular-motion-satellite');
        const previous = select.value;
        const satellites = Array.isArray(state.satellites) ? state.satellites : [];
        if (satellites.length === 0) {
            select.innerHTML = '<option value="">Нет доступных КА</option>';
            state.selectedSatellite = null;
            el('angular-motion-satellite-meta').textContent = 'Для заданного времени нет КА с ПДЦМ.';
            return;
        }
        select.innerHTML = satellites.map(satellite => {
            const label = `${satellite.code || satellite.satelliteId} — ${satellite.name || ''}`.trim();
            return `<option value="${escapeHtml(satellite.satelliteId)}">${escapeHtml(label)}</option>`;
        }).join('');
        if (satellites.some(item => String(item.satelliteId) === previous)) {
            select.value = previous;
        }
        state.selectedSatellite = satellites.find(item => String(item.satelliteId) === select.value) || satellites[0];
        ensureTimeInsideSelectedSatelliteAvailability();
        renderSatelliteMeta();
    }

    function ensureTimeInsideSelectedSatelliteAvailability() {
        const sat = state.selectedSatellite;
        if (!sat?.availabilityStart || !sat?.availabilityStop) return;

        const current = dateTimeMillis(localDateTimeValue());
        const start = dateTimeMillis(sat.availabilityStart);
        const stop = dateTimeMillis(sat.availabilityStop);
        if (!Number.isFinite(current) || current < start || current > stop) {
            el('angular-motion-time').value = toDateTimeInputValue(sat.availabilityStart);
        }
    }

    function renderSatelliteMeta() {
        const sat = state.selectedSatellite;
        el('angular-motion-satellite-meta').textContent = sat
            ? `Доступность ПДЦМ: ${formatDateTime(sat.availabilityStart)} — ${formatDateTime(sat.availabilityStop)}`
            : '';
    }

    async function loadFlightLine() {
        const satelliteId = el('angular-motion-satellite').value;
        const time = localDateTimeValue();
        if (!satelliteId || !time) {
            state.flightLine = [];
            drawMap();
            return;
        }
        state.flightLine = await requestJson(`/api/angular-motion/satellites/${satelliteId}/flight-line?time=${encodeURIComponent(time)}`);
        el('angular-motion-map-meta').textContent = state.flightLine.length > 0
            ? `Трасса и полосы обзора на интервале ${formatDateTime(time)} ± 5 минут.`
            : 'Нет данных трассы полета для выбранного КА и времени.';
        drawMap('flight-line');
    }

    function initMap() {
        if (typeof Cesium === 'undefined') {
            el('angular-motion-map-meta').textContent = 'Библиотека Cesium не загружена.';
            return;
        }

        Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(20, 35, 190, 80);
        state.viewer = new Cesium.Viewer('angular-motion-map', {
            animation: false,
            timeline: false,
            fullscreenButton: false,
            geocoder: false,
            homeButton: false,
            infoBox: false,
            navigationHelpButton: false,
            sceneModePicker: false,
            selectionIndicator: false,
            baseLayerPicker: false,
            sceneMode: Cesium.SceneMode.SCENE2D,
            ...naturalEarthLayerOptions(),
        });
        state.viewer.scene.globe.enableLighting = false;
        state.viewer.scene.requestRenderMode = false;
        state.mapDataSource = new Cesium.CustomDataSource('angular-motion-map-data');
        state.viewer.dataSources.add(state.mapDataSource);
        initCursorCoordinates();
        initTargetOverlay();
        state.pickHandler = new Cesium.ScreenSpaceEventHandler(state.viewer.scene.canvas);
        state.pickHandler.setInputAction(function (movement) {
            if (!state.picking) return;
            resizeMap();
            const cartesian = pickGlobePoint(movement.position);
            if (!cartesian) return;
            const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
            el('angular-motion-lat').value = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
            el('angular-motion-lon').value = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6);
            setPicking(false);
            drawMap();
            updateTargetOverlay(movement.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
        initMapResizeSync();
        state.viewer.scene.postRender.addEventListener(() => updateTargetOverlay());
        drawMap();
    }

    function initMapResizeSync() {
        window.addEventListener('resize', scheduleMapResize);
        if (typeof ResizeObserver !== 'undefined') {
            state.resizeObserver = new ResizeObserver(scheduleMapResize);
            state.resizeObserver.observe(el('angular-motion-map'));
        }
        scheduleMapResize();
    }

    function scheduleMapResize() {
        resizeMap();
        requestAnimationFrame(() => {
            resizeMap();
            requestAnimationFrame(resizeMap);
        });
        setTimeout(resizeMap, 100);
    }

    function resizeMap() {
        if (!state.viewer) return;
        state.viewer.resize();
        state.viewer.scene.requestRender();
    }

    function initCursorCoordinates() {
        state.cursorCoordsPanel = document.createElement('div');
        state.cursorCoordsPanel.className = 'angular-motion-cursor-coords-panel';
        state.cursorCoordsPanel.textContent = 'Координаты: наведите курсор на карту';
        state.viewer.cesiumWidget.container.appendChild(state.cursorCoordsPanel);

        state.viewer.scene.canvas.addEventListener('mousemove', event => {
            const rect = state.viewer.scene.canvas.getBoundingClientRect();
            const screenPosition = new Cesium.Cartesian2(
                event.clientX - rect.left,
                event.clientY - rect.top
            );
            state.cursorCoordsPanel.textContent = formatCursorCoordinates(screenPosition);
        });

        state.viewer.scene.canvas.addEventListener('mouseleave', () => {
            state.cursorCoordsPanel.textContent = 'Координаты: курсор вне карты';
        });
    }

    function initTargetOverlay() {
        state.targetOverlay = document.createElement('div');
        state.targetOverlay.className = 'angular-motion-target-marker';
        state.targetOverlay.innerHTML = '<span class="angular-motion-target-marker__label">ЦЛМ</span><span class="angular-motion-target-marker__dot"></span>';
        state.viewer.cesiumWidget.container.appendChild(state.targetOverlay);
    }

    function naturalEarthLayerOptions() {
        const url = '/webjars/cesium/1.107.2/Build/Cesium/Assets/Textures/NaturalEarthII';
        if (Cesium.ImageryLayer?.fromProviderAsync && Cesium.TileMapServiceImageryProvider?.fromUrl) {
            return {
                baseLayer: Cesium.ImageryLayer.fromProviderAsync(
                    Cesium.TileMapServiceImageryProvider.fromUrl(url)
                ),
            };
        }

        return {
            imageryProvider: new Cesium.TileMapServiceImageryProvider({ url }),
        };
    }

    function pickGlobePoint(position) {
        if (!state.viewer || !position) return null;
        const scene = state.viewer.scene;
        const ray = state.viewer.camera.getPickRay(position);
        if (ray) {
            const globePoint = scene.globe.pick(ray, scene);
            if (Cesium.defined(globePoint)) return globePoint;
        }
        return state.viewer.camera.pickEllipsoid(position, scene.globe.ellipsoid);
    }

    function formatDegree(value, positiveSuffix, negativeSuffix) {
        if (!Number.isFinite(value)) {
            return '-';
        }

        const suffix = value >= 0 ? positiveSuffix : negativeSuffix;
        return `${Math.abs(value).toFixed(6)}° ${suffix}`;
    }

    function formatCursorCoordinates(screenPosition) {
        const point = pickGlobePoint(screenPosition);
        if (!Cesium.defined(point)) {
            return 'Координаты: -';
        }

        const cartographic = state.viewer.scene.globe.ellipsoid.cartesianToCartographic(point);
        if (!Cesium.defined(cartographic)) {
            return 'Координаты: -';
        }

        const lon = Cesium.Math.toDegrees(cartographic.longitude);
        const lat = Cesium.Math.toDegrees(cartographic.latitude);
        return `Lon: ${formatDegree(lon, 'E', 'W')} | Lat: ${formatDegree(lat, 'N', 'S')}`;
    }

    function validDegreesArray(points, latKey, lonKey) {
        const result = [];
        (Array.isArray(points) ? points : []).forEach(point => {
            const lat = Number(point[latKey]);
            const lon = Number(point[lonKey]);
            if (Number.isFinite(lat) && Number.isFinite(lon)) {
                result.push(lon, lat);
            }
        });
        return result;
    }

    function addPolyline(points, latKey, lonKey, color, width, name) {
        if (!state.mapDataSource) return;
        const degrees = validDegreesArray(points, latKey, lonKey);
        if (degrees.length < 4) return;
        state.mapDataSource.entities.add({
            name,
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray(degrees),
                width,
                material: color,
                clampToGround: true,
            },
        });
    }

    function polygonCoordinatesFromWkt(wkt) {
        const textValue = text(wkt).trim();
        const match = textValue.match(/^POLYGON\s*\(\((.*)\)\)$/i);
        if (!match) return [];

        return match[1].split(',')
            .map(item => item.trim().split(/\s+/).map(Number))
            .filter(pair => pair.length >= 2 && Number.isFinite(pair[0]) && Number.isFinite(pair[1]))
            .map(pair => ({ lon: pair[0], lat: pair[1] }));
    }

    function addSurveyContour() {
        if (!state.mapDataSource || !state.result?.contourWkt) return;
        const coordinates = polygonCoordinatesFromWkt(state.result.contourWkt);
        if (coordinates.length < 4) return;

        const degrees = [];
        coordinates.forEach(point => {
            degrees.push(point.lon, point.lat);
        });
        const positions = Cesium.Cartesian3.fromDegreesArray(degrees);
        state.mapDataSource.entities.add({
            name: 'Контур съемки',
            polygon: {
                hierarchy: new Cesium.PolygonHierarchy(positions.slice(0, -1)),
                material: Cesium.Color.ORANGE.withAlpha(0.22),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
        });
        state.mapDataSource.entities.add({
            name: 'Граница контура съемки',
            polyline: {
                positions,
                width: 2,
                material: Cesium.Color.ORANGE,
                clampToGround: true,
            },
        });
    }

    function updateTargetOverlay(screenPosition = null) {
        if (!state.viewer || !state.targetOverlay) return;
        const lat = Number(el('angular-motion-lat').value);
        const lon = Number(el('angular-motion-lon').value);
        if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
            state.targetOverlay.style.display = 'none';
            return;
        }

        const position = screenPosition || Cesium.SceneTransforms.wgs84ToWindowCoordinates(
            state.viewer.scene,
            Cesium.Cartesian3.fromDegrees(lon, lat, 0)
        );
        if (!Cesium.defined(position)) {
            state.targetOverlay.style.display = 'none';
            return;
        }

        state.targetOverlay.style.display = 'block';
        state.targetOverlay.style.left = `${position.x}px`;
        state.targetOverlay.style.top = `${position.y}px`;
    }

    function flightLineBounds() {
        const points = Array.isArray(state.flightLine) ? state.flightLine : [];
        const lats = [];
        const lons = [];
        points.forEach(point => {
            ['latitudeDeg', 'leftLatitudeDeg', 'rightLatitudeDeg'].forEach(key => {
                const value = Number(point[key]);
                if (Number.isFinite(value)) lats.push(value);
            });
            ['longitudeDeg', 'leftLongitudeDeg', 'rightLongitudeDeg'].forEach(key => {
                const value = Number(point[key]);
                if (Number.isFinite(value)) lons.push(value);
            });
        });
        return { lats, lons };
    }

    function fitMapToFlightLine() {
        if (!state.viewer) return;
        const { lats, lons } = flightLineBounds();
        if (lats.length === 0 || lons.length === 0) return;
        const minLat = Math.max(-89.0, Math.min(...lats) - 1.0);
        const maxLat = Math.min(89.0, Math.max(...lats) + 1.0);
        const minLon = Math.max(-180.0, Math.min(...lons) - 1.0);
        const maxLon = Math.min(180.0, Math.max(...lons) + 1.0);
        if (maxLon - minLon > 180.0) return;
        state.viewer.camera.flyTo({
            destination: Cesium.Rectangle.fromDegrees(minLon, minLat, maxLon, maxLat),
            duration: 0.4,
        });
    }

    function centerMapOnTarget() {
        if (!state.viewer) return;
        const lat = Number(el('angular-motion-lat').value);
        const lon = Number(el('angular-motion-lon').value);
        if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;

        const currentHeight = state.viewer.camera.positionCartographic?.height;
        const height = Number.isFinite(currentHeight) ? currentHeight : 3_000_000;
        state.viewer.camera.flyTo({
            destination: Cesium.Cartesian3.fromDegrees(lon, lat, height),
            duration: 0.4,
        });
    }

    function drawMap(centerMode = null) {
        if (!state.mapDataSource || typeof Cesium === 'undefined') return;
        resizeMap();
        state.mapDataSource.entities.removeAll();
        const points = Array.isArray(state.flightLine) ? state.flightLine : [];
        addPolyline(points, 'leftLatitudeDeg', 'leftLongitudeDeg', Cesium.Color.CORNFLOWERBLUE.withAlpha(0.85), 2, 'Левая граница полосы');
        addPolyline(points, 'rightLatitudeDeg', 'rightLongitudeDeg', Cesium.Color.CORNFLOWERBLUE.withAlpha(0.85), 2, 'Правая граница полосы');
        addPolyline(points, 'latitudeDeg', 'longitudeDeg', Cesium.Color.YELLOW, 3, 'Трасса полета');
        addSurveyContour();
        updateTargetOverlay();
        if (centerMode === 'flight-line') {
            fitMapToFlightLine();
        } else if (centerMode === 'target') {
            centerMapOnTarget();
        }
        requestMapRender();
    }

    function requestMapRender() {
        if (!state.viewer) return;
        state.viewer.scene.requestRender();
        requestAnimationFrame(() => {
            state.viewer?.scene.requestRender();
            requestAnimationFrame(() => state.viewer?.scene.requestRender());
        });
    }

    function setPicking(value) {
        state.picking = value;
        el('angular-motion-map').classList.toggle('angular-motion-pick-active', value);
        el('angular-motion-pick-state').textContent = value
            ? 'Кликните по карте, чтобы перенести координаты.'
            : 'Режим выбора координат выключен.';
    }

    function modeChanged() {
        const mode = el('angular-motion-mode').value;
        const sdiVisible = mode === 'AZIMUTH' || mode === 'SMOOTH_SDI';
        document.querySelectorAll('.angular-motion-sdi-fields').forEach(item => item.classList.toggle('d-none', !sdiVisible));
        document.querySelectorAll('.angular-motion-azimuth-field').forEach(item => item.classList.toggle('d-none', mode === 'CONST_ORIENT'));
    }

    function payloadFromForm() {
        ensureTimeInsideSelectedSatelliteAvailability();
        const mode = el('angular-motion-mode').value;
        const payload = {
            satelliteId: Number(el('angular-motion-satellite').value),
            approximateTime: localDateTimeValue(),
            mode,
            latitudeDeg: Number(el('angular-motion-lat').value),
            longitudeDeg: Number(el('angular-motion-lon').value),
            heightM: Number(el('angular-motion-height').value || 0),
            durationSec: Number(el('angular-motion-duration').value),
            captureAngleDeg: Number(el('angular-motion-capture-angle').value || 1.5),
            leadAngleDeg: Number(el('angular-motion-lead-angle').value || 0),
            azimuthDeg: Number(el('angular-motion-azimuth').value || 0),
            pointInCenter: false,
            focusMm: Number(el('angular-motion-focus').value || 5500),
            stepPuudSec: Number(el('angular-motion-step').value || 0.125),
        };
        if (mode === 'AZIMUTH' || mode === 'SMOOTH_SDI') {
            payload.sdi = Number(el('angular-motion-sdi').value);
        }
        return payload;
    }

    function renderResult(result) {
        state.result = result;
        const rows = Array.isArray(result?.points) ? result.points : [];
        el('angular-motion-result-meta').textContent = rows.length > 0
            ? `Старт режима: ${formatDateTime(result.startTime)}. Точек: ${rows.length}. Контур: ${result?.contourWkt ? 'построен' : 'нет'}.`
            : 'Расчет не вернул точек.';
        el('angular-motion-export-csv').disabled = rows.length === 0;
        const body = el('angular-motion-result-body');
        if (rows.length === 0) {
            body.innerHTML = '<tr><td colspan="17" class="text-center text-muted py-4">Нет данных</td></tr>';
            return;
        }
        body.innerHTML = rows.map(point => `
            <tr>
                <td>${escapeHtml(formatDateTime(point.time))}</td>
                <td>${escapeHtml(point.revolution)}</td>
                <td>${escapeHtml(formatNumber(point.tangDeg, 3))}</td>
                <td>${escapeHtml(formatNumber(point.krenDeg, 3))}</td>
                <td>${escapeHtml(formatNumber(point.riskDeg, 3))}</td>
                <td>${escapeHtml(formatNumber(point.groundLatitudeDeg, 5))}</td>
                <td>${escapeHtml(formatNumber(point.groundLongitudeDeg, 5))}</td>
                <td>${escapeHtml(formatNumber(point.omegaX, 7))}</td>
                <td>${escapeHtml(formatNumber(point.omegaY, 7))}</td>
                <td>${escapeHtml(formatNumber(point.omegaZ, 7))}</td>
                <td>${escapeHtml(formatNumber(point.wdX, 7))}</td>
                <td>${escapeHtml(formatNumber(point.wdY, 7))}</td>
                <td>${escapeHtml(formatNumber(point.wdZ, 7))}</td>
                <td>${escapeHtml(formatNumber(point.sdi, 4))}</td>
                <td>${escapeHtml(formatNumber(point.sdiLeft, 4))}</td>
                <td>${escapeHtml(formatNumber(point.sdiRight, 4))}</td>
                <td>${escapeHtml(formatNumber(point.sdiSpread, 4))}</td>
            </tr>
        `).join('');
        drawMap('target');
    }

    async function calculate(event) {
        event.preventDefault();
        showAlert('');
        setLoading(true);
        try {
            const result = await requestJson('/api/angular-motion/calculate', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify(payloadFromForm()),
            });
            renderResult(result);
            showAlert('Расчет ПУУД выполнен.', 'success');
        } catch (error) {
            showAlert(error.message || String(error));
        } finally {
            setLoading(false);
        }
    }

    function exportCsv() {
        const rows = Array.isArray(state.result?.points) ? state.result.points : [];
        if (rows.length === 0) return;
        const header = ['time', 'revolution', 'tangDeg', 'krenDeg', 'riskDeg', 'groundLatitudeDeg', 'groundLongitudeDeg', 'omegaX', 'omegaY', 'omegaZ', 'wdX', 'wdY', 'wdZ', 'sdi', 'sdiLeft', 'sdiRight', 'sdiSpread'];
        const lines = [header.join(';')];
        rows.forEach(row => {
            lines.push(header.map(key => row[key] ?? '').join(';'));
        });
        const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'angular-motion.csv';
        a.click();
        URL.revokeObjectURL(url);
    }

    function init() {
        el('angular-motion-time').value = nowLocalInput();
        initMap();
        el('angular-motion-form').addEventListener('submit', calculate);
        el('angular-motion-refresh-satellites').addEventListener('click', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
        el('angular-motion-time').addEventListener('change', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
        el('angular-motion-satellite').addEventListener('change', () => {
            state.selectedSatellite = state.satellites.find(item => String(item.satelliteId) === el('angular-motion-satellite').value) || null;
            ensureTimeInsideSelectedSatelliteAvailability();
            renderSatelliteMeta();
            loadFlightLine().catch(error => showAlert(error.message || String(error)));
        });
        el('angular-motion-mode').addEventListener('change', modeChanged);
        el('angular-motion-pick-from-map').addEventListener('click', () => setPicking(!state.picking));
        ['angular-motion-lat', 'angular-motion-lon'].forEach(id => el(id).addEventListener('input', () => drawMap()));
        el('angular-motion-export-csv').addEventListener('click', exportCsv);
        modeChanged();
        loadSatellites().catch(error => showAlert(error.message || String(error)));
    }

    document.addEventListener('DOMContentLoaded', init);
})();
