// DSO IndexedDB Web Worker - Runs in background thread to avoid UI blocking
const DB_NAME = 'AstroManagerDSO';
const DB_VERSION = 1;
const REQUIRED_STORES = ['dsoObjects', 'dsoMetadata'];

let db = null;
let normalSearchCache = null;
const NORMAL_SEARCH_CACHE_MAX_MATCHES = 50000;

function ensureSchema(database) {
    if (!database.objectStoreNames.contains('dsoObjects')) {
        const store = database.createObjectStore('dsoObjects', { keyPath: 'id' });
        store.createIndex('identifier', 'identifier', { unique: false });
        store.createIndex('constellation', 'constellation', { unique: false });
        store.createIndex('baseType', 'baseType', { unique: false });
        store.createIndex('magnitudeV', 'magnitudeV', { unique: false });
    }

    if (!database.objectStoreNames.contains('dsoMetadata')) {
        database.createObjectStore('dsoMetadata', { keyPath: 'key' });
    }
}

function hasRequiredStores(database) {
    return REQUIRED_STORES.every(storeName => database.objectStoreNames.contains(storeName));
}

function openDatabase(version = undefined) {
    return new Promise((resolve, reject) => {
        const request = version == null
            ? indexedDB.open(DB_NAME)
            : indexedDB.open(DB_NAME, version);
        
        request.onerror = () => reject(request.error);
        request.onsuccess = () => {
            resolve(request.result);
        };
        
        request.onupgradeneeded = (event) => {
            ensureSchema(event.target.result);
        };
    });
}

// Initialize IndexedDB
async function initDatabase() {
    if (db && hasRequiredStores(db)) {
        return db;
    }

    if (db) {
        db.close();
        db = null;
    }

    let database = await openDatabase();
    if (!hasRequiredStores(database)) {
        const repairedVersion = Math.max((database.version || DB_VERSION) + 1, DB_VERSION + 1);
        console.warn(`[DSO Worker] Missing required stores detected. Repairing schema by upgrading DB to version ${repairedVersion}.`);
        database.close();
        database = await openDatabase(repairedVersion);
    }

    db = database;
    return db;
}

// Fetch and store DSO data
async function fetchAndStoreDsoData(apiUrl) {
    try {
        postMessage({ type: 'progress', message: 'Connecting to server...', percent: 5 });
        
        // Fetch data from API
        const response = await fetch(apiUrl + 'api/admin/dso-static/dso-data.json');
        
        if (!response.ok) {
            // Fallback to dynamic endpoint
            postMessage({ type: 'progress', message: 'Trying fallback endpoint...', percent: 10 });
            const fallbackResponse = await fetch(apiUrl + 'api/admin/dso-offline/data');
            if (!fallbackResponse.ok) {
                throw new Error('Failed to fetch DSO data from both endpoints');
            }
            return await processResponse(fallbackResponse);
        }
        
        return await processResponse(response);
    } catch (error) {
        postMessage({ type: 'error', message: error.message });
        throw error;
    }
}

async function processResponse(response) {
    postMessage({ type: 'progress', message: 'Downloading data...', percent: 15 });
    
    const objects = await response.json();
    const totalObjects = objects.length;
    
    postMessage({ type: 'progress', message: `Downloaded ${totalObjects} objects, storing...`, percent: 30 });
    
    // Ensure database is initialized
    if (!db) {
        await initDatabase();
    }
    
    // Clear existing data and store new data in a single transaction
    const transaction = db.transaction(['dsoObjects'], 'readwrite');
    const store = transaction.objectStore('dsoObjects');
    
    // Clear first
    await new Promise((resolve, reject) => {
        const clearRequest = store.clear();
        clearRequest.onsuccess = resolve;
        clearRequest.onerror = () => reject(clearRequest.error);
    });
    
    postMessage({ type: 'progress', message: 'Storing objects...', percent: 35 });
    
    // Store all objects - use put for better performance
    let stored = 0;
    const batchSize = 2000; // Larger batches in worker thread
    
    for (let i = 0; i < objects.length; i += batchSize) {
        const batch = objects.slice(i, i + batchSize);
        
        await new Promise((resolve, reject) => {
            const batchTransaction = db.transaction(['dsoObjects'], 'readwrite');
            const batchStore = batchTransaction.objectStore('dsoObjects');
            
            batch.forEach(obj => {
                // Ensure ID compatibility
                if (obj.Id && !obj.id) obj.id = obj.Id;
                batchStore.put(obj);
            });
            
            batchTransaction.oncomplete = resolve;
            batchTransaction.onerror = () => reject(batchTransaction.error);
        });
        
        stored += batch.length;
        const percent = 35 + Math.round((stored / totalObjects) * 60);
        postMessage({ type: 'progress', message: `Stored ${stored}/${totalObjects}...`, percent });
    }
    
    postMessage({ type: 'progress', message: 'Finalizing...', percent: 98 });
    normalSearchCache = null;
    
    return { success: true, count: totalObjects };
}

function getNormalSearchCacheKey(filter) {
    const {
        page,
        pageSize,
        ...cacheRelevantFilter
    } = filter || {};

    return JSON.stringify(cacheRelevantFilter);
}

function paginateSearchResults(results, filter) {
    const page = filter.page || 1;
    const pageSize = filter.pageSize || 20;
    const startIndex = (page - 1) * pageSize;

    return results.slice(startIndex, startIndex + pageSize);
}

// Get object count
async function getCount() {
    if (!db) await initDatabase();
    
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['dsoObjects'], 'readonly');
        const store = transaction.objectStore('dsoObjects');
        const request = store.count();
        
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

// Check if data exists (fast)
async function hasData() {
    if (!db) {
        try {
            await initDatabase();
        } catch {
            return false;
        }
    }
    
    return new Promise((resolve) => {
        try {
            const transaction = db.transaction(['dsoObjects'], 'readonly');
            const store = transaction.objectStore('dsoObjects');
            const request = store.openCursor();
            
            request.onsuccess = () => resolve(request.result !== null);
            request.onerror = () => resolve(false);
        } catch {
            resolve(false);
        }
    });
}

// Set database version
async function setVersion(version) {
    if (!db) await initDatabase();
    
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['dsoMetadata'], 'readwrite');
        const store = transaction.objectStore('dsoMetadata');
        const request = store.put({ key: 'databaseVersion', value: version });
        
        request.onsuccess = resolve;
        request.onerror = () => reject(request.error);
    });
}

// Get database version
async function getVersion() {
    if (!db) await initDatabase();
    
    return new Promise((resolve) => {
        try {
            const transaction = db.transaction(['dsoMetadata'], 'readonly');
            const store = transaction.objectStore('dsoMetadata');
            const request = store.get('databaseVersion');
            
            request.onsuccess = () => resolve(request.result?.value || null);
            request.onerror = () => resolve(null);
        } catch {
            resolve(null);
        }
    });
}

// Search DSO objects with filtering - runs entirely in worker thread
async function searchDsoObjects(filter) {
    if (!db) await initDatabase();
    
    const startTime = performance.now();
    const cacheKey = getNormalSearchCacheKey(filter);
    if (normalSearchCache?.key === cacheKey) {
        const pagedResults = paginateSearchResults(normalSearchCache.results, filter);
        const elapsed = Math.round(performance.now() - startTime);
        console.log(`[Worker Search Cache] Reused ${normalSearchCache.totalCount} matches, returning ${pagedResults.length} in ${elapsed}ms`);

        return {
            results: pagedResults,
            totalCount: normalSearchCache.totalCount,
            scanned: 0,
            elapsed,
            cached: true
        };
    }

    const matchingObjects = [];
    let totalScanned = 0;
    
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['dsoObjects'], 'readonly');
        const store = transaction.objectStore('dsoObjects');
        const request = store.openCursor();
        
        request.onsuccess = (event) => {
            const cursor = event.target.result;
            if (cursor) {
                totalScanned++;
                const obj = cursor.value;
                
                // Apply filters
                if (matchesSearchFilter(obj, filter)) {
                    matchingObjects.push(obj);
                }
                
                // Report progress every 10000 items
                if (totalScanned % 10000 === 0) {
                    postMessage({ 
                        type: 'searchProgress', 
                        scanned: totalScanned, 
                        matched: matchingObjects.length 
                    });
                }
                
                cursor.continue();
            } else {
                // Cursor complete - sort and paginate
                sortSearchResults(matchingObjects, filter.sortBy, filter.sortAscending);
                
                if (matchingObjects.length <= NORMAL_SEARCH_CACHE_MAX_MATCHES) {
                    normalSearchCache = {
                        key: cacheKey,
                        results: matchingObjects,
                        totalCount: matchingObjects.length
                    };
                } else {
                    normalSearchCache = null;
                }

                const pagedResults = paginateSearchResults(matchingObjects, filter);
                
                const elapsed = Math.round(performance.now() - startTime);
                console.log(`[Worker Search] Scanned ${totalScanned}, matched ${matchingObjects.length}, returning ${pagedResults.length} in ${elapsed}ms`);
                
                resolve({
                    results: pagedResults,
                    totalCount: matchingObjects.length,
                    scanned: totalScanned,
                    elapsed: elapsed
                });
            }
        };
        
        request.onerror = () => reject(request.error);
    });
}

// Check if object matches search filter
function matchesSearchFilter(obj, filter) {
    // Search term filter (case-insensitive)
    if (filter.searchTerm && filter.searchTerm.trim()) {
        if (!matchesSearchTerms(obj, filter.searchTerm)) {
            return false;
        }
    }

    // Catalog filter
    if (filter.catalogs && filter.catalogs.length > 0) {
        const searchVector = (obj.CombinedSearchVector || obj.combinedSearchVector || '').toLowerCase();
        const identifier = (obj.Identifier || obj.identifier || '').toLowerCase();

        const hasCatalogMatch = filter.catalogs.some(catalog => {
            const catalogLower = (catalog || '').toLowerCase();
            return hasExactCatalogMatch(searchVector, catalogLower) || identifier.startsWith(catalogLower);
        });

        if (!hasCatalogMatch) {
            return false;
        }
    }
    
    // Constellation filter
    if (filter.constellations && filter.constellations.length > 0) {
        const constellation = obj.Constellation || obj.constellation;
        if (!constellation || !filter.constellations.includes(constellation)) {
            return false;
        }
    }
    
    // Object type filter
    if (filter.objectTypes && filter.objectTypes.length > 0) {
        const baseType = obj.BaseType || obj.baseType;
        if (!baseType || !filter.objectTypes.includes(baseType)) {
            return false;
        }
    }

    // Right ascension filter (stored in degrees, filter comes in hours)
    if (filter.raRange && filter.raRange.length === 2) {
        const raMinHours = filter.raRange[0];
        const raMaxHours = filter.raRange[1];

        if (raMinHours > 0 || raMaxHours < 24) {
            const ra = obj.RightAscension ?? obj.rightAscension ?? 0;
            const raMin = raMinHours * 15;
            const raMax = raMaxHours * 15;

            if (raMinHours > raMaxHours) {
                if (!(ra >= raMin || ra <= raMax)) {
                    return false;
                }
            } else if (ra < raMin || ra > raMax) {
                return false;
            }
        }
    }

    // Declination filter
    if (filter.decRange && filter.decRange.length === 2) {
        const decMin = filter.decRange[0];
        const decMax = filter.decRange[1];

        if (decMin > -90 || decMax < 90) {
            const dec = obj.Declination ?? obj.declination ?? 0;
            if (dec < decMin || dec > decMax) {
                return false;
            }
        }
    }
    
    // Magnitude filter
    const magnitude = obj.MagnitudeV ?? obj.magnitudeV;
    if (!filter.includeUnknownMagnitude && (magnitude == null)) {
        return false;
    }
    if (filter.minMagnitude != null && magnitude != null && magnitude < filter.minMagnitude) {
        return false;
    }
    if (filter.maxMagnitude != null && magnitude != null && magnitude > filter.maxMagnitude) {
        return false;
    }
    
    // Size filter
    const size = obj.SizeMaxArcmin ?? obj.sizeMaxArcmin;
    if (!filter.includeUnknownSize && (size == null)) {
        return false;
    }
    if (filter.minSize != null && size != null && size < filter.minSize) {
        return false;
    }
    if (filter.maxSize != null && size != null && size > filter.maxSize) {
        return false;
    }
    
    return true;
}

function calculateAngularDistanceDegrees(ra1Degrees, dec1Degrees, ra2Degrees, dec2Degrees) {
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;

    const ra1Rad = ra1Degrees * degToRad;
    const dec1Rad = dec1Degrees * degToRad;
    const ra2Rad = ra2Degrees * degToRad;
    const dec2Rad = dec2Degrees * degToRad;

    let cosDistance =
        Math.sin(dec1Rad) * Math.sin(dec2Rad) +
        Math.cos(dec1Rad) * Math.cos(dec2Rad) * Math.cos(ra1Rad - ra2Rad);

    cosDistance = Math.max(-1, Math.min(1, cosDistance));
    return Math.acos(cosDistance) * radToDeg;
}

function calculateNearbySearchCoordinateRange(centerRaHours, centerDecDegrees, radiusDegrees) {
    const safeCosDec = Math.max(Math.cos(centerDecDegrees * Math.PI / 180), 0.1);
    const raRadiusHours = (radiusDegrees / safeCosDec) / 15.0;

    let raMin = centerRaHours - raRadiusHours;
    let raMax = centerRaHours + raRadiusHours;

    if (raMin < 0) raMin += 24;
    if (raMax >= 24) raMax -= 24;

    const decMin = Math.max(centerDecDegrees - radiusDegrees, -90);
    const decMax = Math.min(centerDecDegrees + radiusDegrees, 90);

    return { raMin, raMax, decMin, decMax };
}

function matchesNearbyPrefilter(obj, searchNearby) {
    if (!searchNearby || !searchNearby.isActive) {
        return true;
    }

    const { raMin, raMax, decMin, decMax } = calculateNearbySearchCoordinateRange(
        searchNearby.centerRA,
        searchNearby.centerDec,
        searchNearby.searchRadiusDegrees
    );

    const dec = obj.Declination ?? obj.declination ?? 0;
    if (dec < decMin || dec > decMax) {
        return false;
    }

    const raDegrees = obj.RightAscension ?? obj.rightAscension ?? 0;
    const raHours = raDegrees / 15.0;

    if (raMin > raMax) {
        return raHours >= raMin || raHours <= raMax;
    }

    return raHours >= raMin && raHours <= raMax;
}

function delay(ms = 0) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function searchNearbyStream(filter) {
    if (!db) await initDatabase();

    const startTime = performance.now();
    const searchNearby = filter.searchNearby;
    const excludeId = filter.excludeId || null;
    const maxResults = filter.pageSize || 50;
    const nearbyMatches = [];
    let totalScanned = 0;

    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['dsoObjects'], 'readonly');
        const store = transaction.objectStore('dsoObjects');
        const request = store.openCursor();

        request.onsuccess = async (event) => {
            const cursor = event.target.result;
            if (cursor) {
                totalScanned++;
                const obj = cursor.value;

                const objectId = obj.Id || obj.id || null;
                if ((!excludeId || objectId !== excludeId) &&
                    matchesSearchFilter(obj, filter) &&
                    matchesNearbyPrefilter(obj, searchNearby)) {
                    const distance = calculateAngularDistanceDegrees(
                        searchNearby.centerRA * 15.0,
                        searchNearby.centerDec,
                        obj.RightAscension ?? obj.rightAscension ?? 0,
                        obj.Declination ?? obj.declination ?? 0
                    );

                    if (distance <= searchNearby.searchRadiusDegrees) {
                        nearbyMatches.push({
                            ...obj,
                            _distanceDegrees: distance
                        });
                    }
                }

                cursor.continue();
                return;
            }

            nearbyMatches.sort((a, b) => (a._distanceDegrees || 0) - (b._distanceDegrees || 0));
            const finalResults = nearbyMatches.slice(0, maxResults);
            const batchSize = 6;

            for (let i = 0; i < finalResults.length; i += batchSize) {
                const batch = finalResults.slice(i, i + batchSize);
                postMessage({
                    type: 'nearbyBatch',
                    action: 'searchNearbyStream',
                    batch,
                    startIndex: i,
                    totalCount: finalResults.length
                });
                await delay(0);
            }

            const elapsed = Math.round(performance.now() - startTime);
            resolve({
                totalCount: finalResults.length,
                scanned: totalScanned,
                elapsed
            });
        };

        request.onerror = () => reject(request.error);
    });
}

function parseSearchComponents(searchTerm) {
    const searchComponents = [];
    const catalogPrefixes = ['m', 'ngc', 'ic', 'messier', 'sh', 'abell', 'pk'];
    const normalizedTerm = (searchTerm || '').toLowerCase().trim();

    let catalogMatched = false;

    for (const prefix of catalogPrefixes) {
        if (!normalizedTerm.startsWith(prefix)) {
            continue;
        }

        const suffix = normalizedTerm.substring(prefix.length).trim();
        const suffixParts = splitCatalogSuffix(suffix);
        if (suffixParts.length > 0 && suffixParts.every(part => /^\d+$/.test(part))) {
            searchComponents.push(prefix, ...suffixParts);
            catalogMatched = true;
            break;
        }
    }

    if (!catalogMatched && normalizedTerm.length > 0) {
        searchComponents.push(...normalizedTerm.split(/[\s\-_]+/).filter(Boolean));
    }

    return searchComponents;
}

function splitCatalogSuffix(suffix) {
    if (!suffix) return [];

    const parts = [];
    let current = '';
    let currentIsDigit = null;

    const flush = () => {
        if (!current) return;
        parts.push(current);
        current = '';
        currentIsDigit = null;
    };

    for (const ch of suffix) {
        if (!/[a-z0-9]/i.test(ch)) {
            flush();
            continue;
        }

        const isDigit = /\d/.test(ch);
        if (currentIsDigit !== null && currentIsDigit !== isDigit) {
            flush();
        }

        current += ch.toLowerCase();
        currentIsDigit = isDigit;
    }

    flush();
    return parts;
}

function tryNormalizeCatalogSearchKey(searchTerm) {
    if (!searchTerm) return null;

    const compact = searchTerm.toLowerCase().replace(/[^a-z0-9]/g, '');
    if (!compact) return null;

    const hasLetter = /[a-z]/.test(compact);
    const hasDigit = /\d/.test(compact);
    return hasLetter && hasDigit ? compact : null;
}

function buildCompactKey(value) {
    return (value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
}

function hasCompactCatalogMatch(identifier, searchVector, normalizedCatalogSearchKey) {
    if (!normalizedCatalogSearchKey) return false;

    if (buildCompactKey(identifier) === normalizedCatalogSearchKey) {
        return true;
    }

    const terms = (searchVector || '')
        .toLowerCase()
        .split(/\s+/)
        .filter(Boolean)
        .map(buildCompactKey)
        .filter(Boolean);

    for (let start = 0; start < terms.length; start++) {
        let combined = '';
        for (let length = 0; length < 3 && start + length < terms.length; length++) {
            combined += terms[start + length];
            if (combined === normalizedCatalogSearchKey) {
                return true;
            }
        }
    }

    return false;
}

function hasWordStartingWith(text, prefix) {
    if (!text || !prefix) return false;
    return text
        .split(/[\s\-_]+/)
        .filter(Boolean)
        .some(word => word.startsWith(prefix));
}

function hasExactWord(text, word) {
    if (!text || !word) return false;
    return text
        .split(/[\s\-_]+/)
        .filter(Boolean)
        .some(candidate => candidate === word);
}

function hasExactCatalogMatch(searchVector, catalog) {
    if (!searchVector || !catalog) return false;
    const catalogLower = catalog.toLowerCase().replace(/ /g, '_');
    const terms = searchVector.toLowerCase().split(/\s+/).filter(Boolean);
    return terms.some(term => term === catalogLower);
}

function matchesSearchTerms(obj, searchTerm) {
    const searchComponents = parseSearchComponents(searchTerm);
    if (searchComponents.length === 0) return true;

    const deepSearch = searchTerm.endsWith('+') || searchTerm.endsWith('*') || searchComponents.length === 1;
    const identifier = (obj.Identifier || obj.identifier || '').toLowerCase();
    const searchVector = (obj.CombinedSearchVector || obj.combinedSearchVector || '').toLowerCase();
    const searchableText = `${identifier} ${searchVector}`.trim();
    const normalizedCatalogSearchKey = tryNormalizeCatalogSearchKey(searchTerm);

    if (hasCompactCatalogMatch(identifier, searchVector, normalizedCatalogSearchKey)) {
        return true;
    }

    if (deepSearch) {
        return searchComponents.some(component =>
            identifier.startsWith(component) ||
            searchVector.includes(component) ||
            hasWordStartingWith(searchableText, component));
    }

    return searchComponents.every(component =>
        identifier === component ||
        hasExactWord(searchVector, component));
}

// Enhanced search with observability filtering
async function searchDsoObjectsWithObservability(filter) {
    if (!db) await initDatabase();
    
    const startTime = performance.now();
    const matchingObjects = [];
    let totalScanned = 0;
    
    // Observability parameters
    const hasObservability = filter.observatory && filter.observationStartTime && filter.observationEndTime;
    const minAltitude = filter.minAltitude || 20; // Default 20 degrees
    const minObservableMinutes = filter.minObservableMinutes || 0;
    const timeStepMinutes = 30; // 30-minute steps for speed
    
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['dsoObjects'], 'readonly');
        const store = transaction.objectStore('dsoObjects');
        const request = store.openCursor();
        
        request.onsuccess = (event) => {
            const cursor = event.target.result;
            if (cursor) {
                totalScanned++;
                const obj = cursor.value;
                
                // Apply basic filters first (fast)
                if (matchesSearchFilter(obj, filter)) {
                    // Apply observability filter if needed
                    if (hasObservability) {
                        const ra = (obj.RightAscension || obj.rightAscension) * 15; // Convert hours to degrees
                        const dec = obj.Declination || obj.declination;
                        
                        const observableMinutes = isObservableDuring(
                            ra, dec,
                            filter.observatory.latitude,
                            filter.observatory.longitude,
                            filter.observationStartTime,
                            filter.observationEndTime,
                            minAltitude,
                            timeStepMinutes
                        );
                        
                        if (observableMinutes >= minObservableMinutes) {
                            obj._observableMinutes = observableMinutes;
                            matchingObjects.push(obj);
                        }
                    } else {
                        matchingObjects.push(obj);
                    }
                }
                
                // Report progress every 10000 items
                if (totalScanned % 10000 === 0) {
                    postMessage({ 
                        type: 'searchProgress', 
                        scanned: totalScanned, 
                        matched: matchingObjects.length 
                    });
                }
                
                cursor.continue();
            } else {
                // Cursor complete - sort and paginate
                if (hasObservability && (filter.sortBy === 7 || filter.sortBy === 'ObservabilityDuration')) {
                    // Sort by observable minutes descending
                    matchingObjects.sort((a, b) => (b._observableMinutes || 0) - (a._observableMinutes || 0));
                } else {
                    sortSearchResults(matchingObjects, filter.sortBy, filter.sortAscending);
                }
                
                const page = filter.page || 1;
                const pageSize = filter.pageSize || 20;
                const startIndex = (page - 1) * pageSize;
                const pagedResults = matchingObjects.slice(startIndex, startIndex + pageSize);
                
                const elapsed = Math.round(performance.now() - startTime);
                console.log(`[Worker Observability Search] Scanned ${totalScanned}, matched ${matchingObjects.length}, returning ${pagedResults.length} in ${elapsed}ms`);
                
                resolve({
                    results: pagedResults,
                    totalCount: matchingObjects.length,
                    scanned: totalScanned,
                    elapsed: elapsed
                });
            }
        };
        
        request.onerror = () => reject(request.error);
    });
}

// Calculate altitude of an object at a given time
function calculateAltitude(ra, dec, lat, lon, dateTime) {
    // Convert to radians
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;
    
    // Calculate Julian Date
    const jd = getJulianDate(dateTime);
    
    // Calculate Local Sidereal Time
    const lst = getLocalSiderealTime(jd, lon);
    
    // Calculate Hour Angle
    let ha = lst - ra;
    if (ha < 0) ha += 360;
    if (ha > 180) ha -= 360;
    
    // Convert to radians
    const haRad = ha * degToRad;
    const decRad = dec * degToRad;
    const latRad = lat * degToRad;
    
    // Calculate altitude
    const sinAlt = Math.sin(decRad) * Math.sin(latRad) + 
                   Math.cos(decRad) * Math.cos(latRad) * Math.cos(haRad);
    const altitude = Math.asin(sinAlt) * radToDeg;
    
    return altitude;
}

// Get Julian Date from JavaScript Date
function getJulianDate(date) {
    const y = date.getUTCFullYear();
    const m = date.getUTCMonth() + 1;
    const d = date.getUTCDate();
    const h = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600;
    
    let jy = y;
    let jm = m;
    if (m <= 2) {
        jy--;
        jm += 12;
    }
    
    const a = Math.floor(jy / 100);
    const b = 2 - a + Math.floor(a / 4);
    
    const jd = Math.floor(365.25 * (jy + 4716)) + 
               Math.floor(30.6001 * (jm + 1)) + 
               d + h / 24 + b - 1524.5;
    
    return jd;
}

// Parse incoming DateTime values consistently with backend behavior.
// Backend treats DateTimeKind.Unspecified as UTC, so timezone-less ISO strings
// must be interpreted as UTC (not browser local time).
function parseAstroDateTime(value, timezoneOffsetMinutes) {
    if (value instanceof Date) return value;
    if (typeof value === 'number') return new Date(value);
    if (typeof value !== 'string') return new Date(value);

    const hasTimezone = /([zZ]|[+\-]\d{2}:\d{2})$/.test(value);
    if (hasTimezone) {
        return new Date(value);
    }

    // For observatory-local wall-clock strings (no timezone), convert using the provided
    // observatory UTC offset so local observatory times remain stable across client timezones.
    if (Number.isFinite(timezoneOffsetMinutes)) {
        const asUtcWallClock = new Date(`${value}Z`);
        return new Date(asUtcWallClock.getTime() - (timezoneOffsetMinutes * 60 * 1000));
    }

    return new Date(`${value}Z`);
}

// Get Local Sidereal Time in degrees
function getLocalSiderealTime(jd, longitude) {
    const t = (jd - 2451545.0) / 36525.0;
    
    // Greenwich Mean Sidereal Time
    let gmst = 280.46061837 + 360.98564736629 * (jd - 2451545.0) + 
               0.000387933 * t * t - t * t * t / 38710000.0;
    
    // Normalize to 0-360
    gmst = gmst % 360;
    if (gmst < 0) gmst += 360;
    
    // Add longitude for Local Sidereal Time
    let lst = gmst + longitude;
    lst = lst % 360;
    if (lst < 0) lst += 360;
    
    return lst;
}

// ============================================
// Moon Position Calculations (matches AASharp)
// ============================================

// Calculate Moon's ecliptic longitude (simplified algorithm from Meeus)
function getMoonEclipticLongitude(jd) {
    const T = (jd - 2451545.0) / 36525.0;
    const degToRad = Math.PI / 180;
    
    // Mean longitude of Moon
    const Lp = 218.3164477 + 481267.88123421 * T - 0.0015786 * T * T + T * T * T / 538841 - T * T * T * T / 65194000;
    
    // Mean elongation of Moon
    const D = 297.8501921 + 445267.1114034 * T - 0.0018819 * T * T + T * T * T / 545868 - T * T * T * T / 113065000;
    
    // Sun's mean anomaly
    const M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T + T * T * T / 24490000;
    
    // Moon's mean anomaly
    const Mp = 134.9633964 + 477198.8675055 * T + 0.0087414 * T * T + T * T * T / 69699 - T * T * T * T / 14712000;
    
    // Moon's argument of latitude
    const F = 93.2720950 + 483202.0175233 * T - 0.0036539 * T * T - T * T * T / 3526000 + T * T * T * T / 863310000;
    
    // Principal terms for longitude
    let sumL = 0;
    sumL += 6288774 * Math.sin(Mp * degToRad);
    sumL += 1274027 * Math.sin((2 * D - Mp) * degToRad);
    sumL += 658314 * Math.sin(2 * D * degToRad);
    sumL += 213618 * Math.sin(2 * Mp * degToRad);
    sumL += -185116 * Math.sin(M * degToRad);
    sumL += -114332 * Math.sin(2 * F * degToRad);
    sumL += 58793 * Math.sin((2 * D - 2 * Mp) * degToRad);
    sumL += 57066 * Math.sin((2 * D - M - Mp) * degToRad);
    sumL += 53322 * Math.sin((2 * D + Mp) * degToRad);
    sumL += 45758 * Math.sin((2 * D - M) * degToRad);
    sumL += -40923 * Math.sin((M - Mp) * degToRad);
    sumL += -34720 * Math.sin(D * degToRad);
    sumL += -30383 * Math.sin((M + Mp) * degToRad);
    sumL += 15327 * Math.sin((2 * D - 2 * F) * degToRad);
    sumL += -12528 * Math.sin((Mp + 2 * F) * degToRad);
    sumL += 10980 * Math.sin((Mp - 2 * F) * degToRad);
    
    let longitude = Lp + sumL / 1000000;
    longitude = longitude % 360;
    if (longitude < 0) longitude += 360;
    
    return longitude;
}

// Calculate Moon's ecliptic latitude
function getMoonEclipticLatitude(jd) {
    const T = (jd - 2451545.0) / 36525.0;
    const degToRad = Math.PI / 180;
    
    const D = 297.8501921 + 445267.1114034 * T - 0.0018819 * T * T;
    const M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T;
    const Mp = 134.9633964 + 477198.8675055 * T + 0.0087414 * T * T;
    const F = 93.2720950 + 483202.0175233 * T - 0.0036539 * T * T;
    
    let sumB = 0;
    sumB += 5128122 * Math.sin(F * degToRad);
    sumB += 280602 * Math.sin((Mp + F) * degToRad);
    sumB += 277693 * Math.sin((Mp - F) * degToRad);
    sumB += 173237 * Math.sin((2 * D - F) * degToRad);
    sumB += 55413 * Math.sin((2 * D - Mp + F) * degToRad);
    sumB += 46271 * Math.sin((2 * D - Mp - F) * degToRad);
    sumB += 32573 * Math.sin((2 * D + F) * degToRad);
    sumB += 17198 * Math.sin((2 * Mp + F) * degToRad);
    sumB += 9266 * Math.sin((2 * D + Mp - F) * degToRad);
    sumB += 8822 * Math.sin((2 * Mp - F) * degToRad);
    
    return sumB / 1000000;
}

// Get obliquity of the ecliptic
function getObliquityOfEcliptic(jd) {
    const T = (jd - 2451545.0) / 36525.0;
    return 23.439291 - 0.0130042 * T - 0.00000016 * T * T + 0.000000504 * T * T * T;
}

// Convert ecliptic coordinates to equatorial (RA, Dec)
function eclipticToEquatorial(eclLon, eclLat, obliquity) {
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;
    
    const lonRad = eclLon * degToRad;
    const latRad = eclLat * degToRad;
    const oblRad = obliquity * degToRad;
    
    // Calculate Right Ascension
    const sinRA = Math.sin(lonRad) * Math.cos(oblRad) - Math.tan(latRad) * Math.sin(oblRad);
    const cosRA = Math.cos(lonRad);
    let ra = Math.atan2(sinRA, cosRA) * radToDeg;
    if (ra < 0) ra += 360;
    
    // Calculate Declination
    const sinDec = Math.sin(latRad) * Math.cos(oblRad) + Math.cos(latRad) * Math.sin(oblRad) * Math.sin(lonRad);
    const dec = Math.asin(sinDec) * radToDeg;
    
    return { ra, dec };
}

// Calculate Moon illuminated fraction (simplified)
function getMoonIlluminatedFraction(jd) {
    const T = (jd - 2451545.0) / 36525.0;
    const degToRad = Math.PI / 180;
    
    // Mean elongation of Moon from Sun
    const D = 297.8501921 + 445267.1114034 * T - 0.0018819 * T * T;
    
    // Sun's mean anomaly
    const M = 357.5291092 + 35999.0502909 * T - 0.0001536 * T * T;
    
    // Moon's mean anomaly
    const Mp = 134.9633964 + 477198.8675055 * T + 0.0087414 * T * T;
    
    // Phase angle (simplified)
    const i = 180 - D - 6.289 * Math.sin(Mp * degToRad) + 
              2.1 * Math.sin(M * degToRad) - 
              1.274 * Math.sin((2 * D - Mp) * degToRad) - 
              0.658 * Math.sin(2 * D * degToRad) - 
              0.214 * Math.sin(2 * Mp * degToRad) - 
              0.11 * Math.sin(D * degToRad);
    
    // Illuminated fraction
    const k = (1 + Math.cos(i * degToRad)) / 2;
    return Math.max(0, Math.min(1, k));
}

// Get full Moon position (RA, Dec, Alt, Az, IlluminatedFraction)
function getMoonPosition(lat, lon, dateTime) {
    const jd = getJulianDate(dateTime);
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;
    
    // Get ecliptic coordinates
    const moonLon = getMoonEclipticLongitude(jd);
    const moonLat = getMoonEclipticLatitude(jd);
    const obliquity = getObliquityOfEcliptic(jd);
    
    // Convert to equatorial
    const equatorial = eclipticToEquatorial(moonLon, moonLat, obliquity);
    const moonRA = equatorial.ra;  // in degrees
    const moonDec = equatorial.dec;
    
    // Calculate altitude and azimuth
    const alt = calculateAltitude(moonRA, moonDec, lat, lon, dateTime);
    const az = calculateAzimuth(moonRA, moonDec, lat, lon, dateTime);
    
    // Get illuminated fraction
    const illuminatedFraction = getMoonIlluminatedFraction(jd);
    
    return {
        rightAscension: moonRA / 15, // Convert degrees to hours for consistency
        declination: moonDec,
        altitude: alt,
        azimuth: az,
        illuminatedFraction: illuminatedFraction
    };
}

// Calculate angular distance between two celestial objects (in degrees)
function calculateAngularDistance(ra1Hours, dec1, ra2Hours, dec2) {
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;
    
    // Convert RA from hours to degrees
    const ra1 = ra1Hours * 15;
    const ra2 = ra2Hours * 15;
    
    // Convert to radians
    const ra1Rad = ra1 * degToRad;
    const ra2Rad = ra2 * degToRad;
    const dec1Rad = dec1 * degToRad;
    const dec2Rad = dec2 * degToRad;
    
    // Use spherical law of cosines
    const cosD = Math.sin(dec1Rad) * Math.sin(dec2Rad) + 
                 Math.cos(dec1Rad) * Math.cos(dec2Rad) * Math.cos(ra1Rad - ra2Rad);
    
    // Clamp to avoid numerical errors
    const cosD_clamped = Math.max(-1, Math.min(1, cosD));
    const distance = Math.acos(cosD_clamped) * radToDeg;
    
    return distance;
}

// Check if object is observable during time window
function isObservableDuring(ra, dec, lat, lon, startTime, endTime, minAltitude, timeStepMinutes = 30) {
    const start = parseAstroDateTime(startTime);
    const end = parseAstroDateTime(endTime);
    const stepMs = timeStepMinutes * 60 * 1000;
    
    let observableMinutes = 0;
    let currentTime = start.getTime();
    
    while (currentTime <= end.getTime()) {
        const date = new Date(currentTime);
        const alt = calculateAltitude(ra, dec, lat, lon, date);
        
        if (alt >= minAltitude) {
            observableMinutes += timeStepMinutes;
        }
        
        currentTime += stepMs;
    }
    
    return observableMinutes;
}

// Calculate chart data for a DSO (altitude over time, with full moon data)
function calculateChartData(params) {
    const startTime = performance.now();
    const dataPoints = [];
    
    const observatoryOffsetMinutes = Number.isFinite(params?.observatory?.timezoneOffsetMinutes)
        ? params.observatory.timezoneOffsetMinutes
        : null;
    const start = parseAstroDateTime(params.startTime, observatoryOffsetMinutes);
    const end = parseAstroDateTime(params.endTime, observatoryOffsetMinutes);
    const stepMs = (params.timeStepMinutes || 5) * 60 * 1000;
    const lat = params.observatory.latitude;
    const lon = params.observatory.longitude;
    const raHours = params.rightAscension; // RA in hours
    const raDeg = raHours * 15; // Convert to degrees
    const dec = params.declination;
    
    let currentTime = start.getTime();
    
    while (currentTime <= end.getTime()) {
        const date = new Date(currentTime);
        const alt = calculateAltitude(raDeg, dec, lat, lon, date);
        const az = calculateAzimuth(raDeg, dec, lat, lon, date);
        
        // Calculate moon position
        const moonPos = getMoonPosition(lat, lon, date);
        
        // Calculate angular distance from DSO to Moon
        const angularDistance = calculateAngularDistance(
            raHours, dec,
            moonPos.rightAscension, moonPos.declination
        );
        
        dataPoints.push({
            time: date.toISOString(),
            altitude: Math.max(0, alt), // Clamp to 0 minimum
            azimuth: az,
            moonPosition: {
                altitude: moonPos.altitude,
                azimuth: moonPos.azimuth,
                illuminatedFraction: moonPos.illuminatedFraction
            },
            angularDistanceToMoon: angularDistance
        });
        
        currentTime += stepMs;
    }
    
    const elapsed = Math.round(performance.now() - startTime);
    console.log(`[Worker Chart] Calculated ${dataPoints.length} points (with moon) in ${elapsed}ms`);
    
    return {
        dataPoints: dataPoints,
        elapsed: elapsed
    };
}

// Calculate chart data for multiple DSOs in batch (with full moon data)
function calculateBatchChartData(params) {
    const startTime = performance.now();
    const results = {};

    const observatoryOffsetMinutes = Number.isFinite(params?.observatory?.timezoneOffsetMinutes)
        ? params.observatory.timezoneOffsetMinutes
        : null;
    const start = parseAstroDateTime(params.startTime, observatoryOffsetMinutes);
    const end = parseAstroDateTime(params.endTime, observatoryOffsetMinutes);
    const stepMs = (params.timeStepMinutes || 5) * 60 * 1000;
    const lat = params.observatory.latitude;
    const lon = params.observatory.longitude;
    
    // Pre-calculate time points once (shared across all DSOs)
    const timePoints = [];
    let currentTime = start.getTime();
    while (currentTime <= end.getTime()) {
        timePoints.push(new Date(currentTime));
        currentTime += stepMs;
    }
    
    // Pre-calculate moon positions for all time points (expensive, do once)
    const moonPositions = timePoints.map(date => getMoonPosition(lat, lon, date));
    
    // Calculate for each DSO
    for (let i = 0; i < params.dsos.length; i++) {
        const dso = params.dsos[i];
        const raDeg = dso.rightAscension * 15; // Convert hours to degrees
        const dec = dso.declination;
        const dataPoints = [];
        
        for (let j = 0; j < timePoints.length; j++) {
            const date = timePoints[j];
            const moonPos = moonPositions[j];
            
            const alt = calculateAltitude(raDeg, dec, lat, lon, date);
            const az = calculateAzimuth(raDeg, dec, lat, lon, date);
            
            // Calculate angular distance from DSO to Moon
            const angularDistance = calculateAngularDistance(
                dso.rightAscension, dec,
                moonPos.rightAscension, moonPos.declination
            );
            
            dataPoints.push({
                time: date.toISOString(),
                altitude: Math.max(0, alt),
                azimuth: az,
                moonPosition: {
                    altitude: moonPos.altitude,
                    azimuth: moonPos.azimuth,
                    illuminatedFraction: moonPos.illuminatedFraction
                },
                angularDistanceToMoon: angularDistance
            });
        }
        
        results[dso.id] = { dataPoints };
    }
    
    const elapsed = Math.round(performance.now() - startTime);
    console.log(`[Worker Batch Chart] Calculated ${params.dsos.length} DSOs x ${timePoints.length} points (with moon) in ${elapsed}ms`);
    
    return {
        results: results,
        elapsed: elapsed
    };
}

// Calculate azimuth of an object
function calculateAzimuth(ra, dec, lat, lon, dateTime) {
    const degToRad = Math.PI / 180;
    const radToDeg = 180 / Math.PI;
    
    const jd = getJulianDate(dateTime);
    const lst = getLocalSiderealTime(jd, lon);
    
    let ha = lst - ra;
    if (ha < 0) ha += 360;
    if (ha > 180) ha -= 360;
    
    const haRad = ha * degToRad;
    const decRad = dec * degToRad;
    const latRad = lat * degToRad;
    
    // Calculate azimuth
    const sinAz = -Math.sin(haRad) * Math.cos(decRad);
    const cosAz = Math.sin(decRad) * Math.cos(latRad) - 
                  Math.cos(decRad) * Math.sin(latRad) * Math.cos(haRad);
    
    let az = Math.atan2(sinAz, cosAz) * radToDeg;
    if (az < 0) az += 360;
    
    return az;
}

// Sort search results
function sortSearchResults(results, sortBy, ascending) {
    const direction = ascending ? 1 : -1;
    
    results.sort((a, b) => {
        let valA, valB;
        
        switch (sortBy) {
            case 'RightAscension':
            case 1:
                valA = a.RightAscension ?? a.rightAscension ?? 0;
                valB = b.RightAscension ?? b.rightAscension ?? 0;
                break;
            case 'Declination':
            case 2:
                valA = a.Declination ?? a.declination ?? 0;
                valB = b.Declination ?? b.declination ?? 0;
                break;
            case 'ObjectType':
            case 3:
                valA = a.BaseType || a.baseType || '';
                valB = b.BaseType || b.baseType || '';
                return direction * valA.localeCompare(valB);
            case 'ObjectName':
            case 4:
                valA = a.Identifier || a.identifier || '';
                valB = b.Identifier || b.identifier || '';
                return direction * valA.localeCompare(valB);
            case 'Size':
            case 5:
                valA = a.SizeMaxArcmin ?? a.sizeMaxArcmin ?? 0;
                valB = b.SizeMaxArcmin ?? b.sizeMaxArcmin ?? 0;
                break;
            case 'Magnitude':
            case 6:
                valA = a.MagnitudeV ?? a.magnitudeV ?? 999;
                valB = b.MagnitudeV ?? b.magnitudeV ?? 999;
                break;
            case 'ObservabilityDuration':
            case 7:
                valA = a._observableMinutes || 0;
                valB = b._observableMinutes || 0;
                break;
            case 'Popularity':
            case 0:
            default:
                valA = a.Popularity ?? a.popularity ?? 0;
                valB = b.Popularity ?? b.popularity ?? 0;
                break;
        }
        
        return direction * (valA - valB);
    });
}

// Message handler
self.onmessage = async function(e) {
    const { action, data } = e.data;
    
    try {
        switch (action) {
            case 'init':
                await initDatabase();
                postMessage({ type: 'complete', action: 'init', success: true });
                break;
                
            case 'fetchAndStore':
                const result = await fetchAndStoreDsoData(data.apiUrl);
                if (data.version) {
                    await setVersion(data.version);
                }
                postMessage({ type: 'complete', action: 'fetchAndStore', success: true, count: result.count });
                break;
                
            case 'hasData':
                const exists = await hasData();
                postMessage({ type: 'complete', action: 'hasData', result: exists });
                break;
                
            case 'getCount':
                const count = await getCount();
                postMessage({ type: 'complete', action: 'getCount', result: count });
                break;
                
            case 'getVersion':
                const version = await getVersion();
                postMessage({ type: 'complete', action: 'getVersion', result: version });
                break;
                
            case 'setVersion':
                await setVersion(data.version);
                postMessage({ type: 'complete', action: 'setVersion', success: true });
                break;
                
            case 'search':
                const searchResult = await searchDsoObjects(data.filter);
                postMessage({ type: 'complete', action: 'search', result: searchResult });
                break;
                
            case 'searchWithObservability':
                const obsResult = await searchDsoObjectsWithObservability(data.filter);
                postMessage({ type: 'complete', action: 'searchWithObservability', result: obsResult });
                break;

            case 'searchNearbyStream':
                const nearbyStreamResult = await searchNearbyStream(data.filter);
                postMessage({ type: 'complete', action: 'searchNearbyStream', result: nearbyStreamResult });
                break;
                
            case 'calculateChartData':
                const chartResult = calculateChartData(data.params);
                postMessage({ type: 'complete', action: 'calculateChartData', result: chartResult });
                break;
                
            case 'calculateBatchChartData':
                const batchChartResult = calculateBatchChartData(data.params);
                postMessage({ type: 'complete', action: 'calculateBatchChartData', result: batchChartResult });
                break;
                
            default:
                postMessage({ type: 'error', message: `Unknown action: ${action}` });
        }
    } catch (error) {
        postMessage({ type: 'error', action, message: error.message });
    }
};

// Signal that worker is ready
postMessage({ type: 'ready' });
