import * as plugins from './plugins.js';
import * as interfaces from './env.js';
import { logger } from './logging.js';
import { ServiceWorker } from './classes.serviceworker.js';
import { getMetricsCollector } from './classes.metrics.js';
import { getEventBus, ServiceWorkerEvent } from './classes.eventbus.js';
import { getErrorHandler, ServiceWorkerErrorType } from './classes.errorhandler.js';
import { getDashboardGenerator } from './classes.dashboard.js';
export class CacheManager {
    losslessServiceWorkerRef;
    usedCacheNames = {
        runtimeCacheName: 'runtime'
    };
    // Request deduplication: tracks in-flight requests to prevent duplicate fetches
    inFlightRequests = new Map();
    INFLIGHT_CLEANUP_INTERVAL = 30000; // 30 seconds
    cleanupIntervalId = null;
    constructor(losslessServiceWorkerRefArg) {
        this.losslessServiceWorkerRef = losslessServiceWorkerRefArg;
        this._setupCache();
        this._setupInFlightCleanup();
    }
    /**
     * Sets up periodic cleanup of stale in-flight request entries
     */
    _setupInFlightCleanup() {
        // Clean up stale entries periodically
        this.cleanupIntervalId = setInterval(() => {
            // The Map should naturally clean up via .finally(), but this is a safety net
            if (this.inFlightRequests.size > 100) {
                logger.log('warn', `In-flight requests map has ${this.inFlightRequests.size} entries, clearing...`);
                this.inFlightRequests.clear();
            }
        }, this.INFLIGHT_CLEANUP_INTERVAL);
    }
    /**
     * Fetches a request with deduplication - coalesces identical concurrent requests
     */
    async fetchWithDeduplication(request) {
        const key = `${request.method}:${request.url}`;
        const metrics = getMetricsCollector();
        const eventBus = getEventBus();
        // Check if we already have an in-flight request for this URL
        const existingRequest = this.inFlightRequests.get(key);
        if (existingRequest) {
            logger.log('note', `Deduplicating request for ${request.url}`);
            try {
                const response = await existingRequest;
                // Clone the response since it may have been consumed
                return response.clone();
            }
            catch (error) {
                // If the original request failed, we should try again
                this.inFlightRequests.delete(key);
                throw error;
            }
        }
        // Record the new request
        metrics.recordRequest(request.url);
        eventBus.emit(ServiceWorkerEvent.NETWORK_REQUEST_START, {
            url: request.url,
            method: request.method,
        });
        const startTime = Date.now();
        // Create a new fetch promise and track it
        const fetchPromise = fetch(request)
            .then(async (response) => {
            const duration = Date.now() - startTime;
            // Try to get response size
            const contentLength = response.headers.get('content-length');
            const bytes = contentLength ? parseInt(contentLength, 10) : 0;
            metrics.recordRequestSuccess(request.url, duration, bytes);
            eventBus.emit(ServiceWorkerEvent.NETWORK_REQUEST_COMPLETE, {
                url: request.url,
                method: request.method,
                status: response.status,
                duration,
                bytes,
            });
            return response;
        })
            .catch((error) => {
            const duration = Date.now() - startTime;
            const errorHandler = getErrorHandler();
            errorHandler.handleNetworkError(`Fetch failed for ${request.url}: ${error?.message || error}`, request.url, error instanceof Error ? error : undefined, { method: request.method, duration });
            metrics.recordRequestFailure(request.url, error?.message);
            eventBus.emit(ServiceWorkerEvent.NETWORK_REQUEST_ERROR, {
                url: request.url,
                method: request.method,
                duration,
                error: error?.message || 'Unknown error',
            });
            throw error;
        })
            .finally(() => {
            // Remove from in-flight requests when done
            this.inFlightRequests.delete(key);
        });
        // Track the in-flight request
        this.inFlightRequests.set(key, fetchPromise);
        return fetchPromise;
    }
    /**
     * Sets up the service worker's fetch event to intercept and cache responses.
     */
    _setupCache = () => {
        // Create a matching request. For internal requests, reuse the original; for external requests, create one with CORS settings.
        const createMatchRequest = (requestArg) => {
            let matchRequest;
            try {
                if (requestArg.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
                    // Internal request
                    matchRequest = requestArg;
                }
                else {
                    // External request: create a new Request with appropriate CORS settings.
                    matchRequest = new Request(requestArg.url, {
                        method: requestArg.method,
                        headers: requestArg.headers,
                        mode: 'cors',
                        credentials: 'same-origin',
                        redirect: 'follow'
                    });
                }
            }
            catch (err) {
                logger.log('error', `Error creating match request for ${requestArg.url}: ${err}`);
                throw err;
            }
            return matchRequest;
        };
        /**
         * Creates a 500 error response.
         */
        const create500Response = async (requestArg, responseArg) => {
            try {
                const responseText = await responseArg.clone().text();
                return new Response(`
          <html>
            <head>
              <style>
                .note {
                  padding: 10px;
                  color: #fff;
                  background: #000;
                  border-bottom: 1px solid #e4002b;
                  margin-bottom: 20px;
                }
              </style>
            </head>
            <body>
              <div class="note">
                <strong>ServiceWorker error 500</strong><br>
              </div>
              ServiceWorker is unable to fetch this request.<br>
              <br>
              <strong>Request URL:</strong> ${requestArg.url}<br>
              <strong>Response Type:</strong> ${responseArg.type}<br>
              <strong>Response Body:</strong> ${responseText}<br>
            </body>
          </html>
        `, {
                    headers: { 'Content-Type': 'text/html' },
                    status: 500
                });
            }
            catch (err) {
                logger.log('error', `Error creating 500 response for ${requestArg.url}: ${err}`);
                return new Response('Internal error', { status: 500 });
            }
        };
        // Listen for fetch events on the service worker's controlled window.
        this.losslessServiceWorkerRef.serviceWindowRef.addEventListener('fetch', async (fetchEventArg) => {
            try {
                const originalRequest = fetchEventArg.request;
                const parsedUrl = new URL(originalRequest.url);
                // Handle dashboard routes - serve directly from service worker
                if (parsedUrl.pathname === '/sw-dash' || parsedUrl.pathname === '/sw-dash/') {
                    const dashboard = getDashboardGenerator();
                    fetchEventArg.respondWith(Promise.resolve(dashboard.serveDashboard()));
                    return;
                }
                // /sw-dash/metrics - THE initial seed endpoint (provides ALL data)
                if (parsedUrl.pathname === '/sw-dash/metrics') {
                    const dashboard = getDashboardGenerator();
                    fetchEventArg.respondWith(dashboard.serveMetrics());
                    return;
                }
                // /sw-dash/speedtest - user-triggered speedtest
                if (parsedUrl.pathname === '/sw-dash/speedtest') {
                    const dashboard = getDashboardGenerator();
                    fetchEventArg.respondWith(dashboard.runSpeedtest());
                    return;
                }
                // /sw-dash/resources - resource data (kept for now, could be merged into metrics)
                if (parsedUrl.pathname === '/sw-dash/resources') {
                    const dashboard = getDashboardGenerator();
                    fetchEventArg.respondWith(Promise.resolve(dashboard.serveResources()));
                    return;
                }
                // All other /sw-dash/* routes removed - use DeesComms instead:
                // - Events: via serviceworker_getEventLog, serviceworker_clearEventLog
                // - Requests: via serviceworker_getTypedRequestLogs, serviceworker_clearTypedRequestLogs
                // Block requests that we don't want the service worker to handle.
                if (parsedUrl.hostname.includes('paddle.com') ||
                    parsedUrl.hostname.includes('paypal.com') ||
                    parsedUrl.hostname.includes('reception.lossless.one') ||
                    parsedUrl.pathname.startsWith('/socket.io') ||
                    originalRequest.url.startsWith('https://umami.')) {
                    logger.log('note', `ServiceWorker not active for ${parsedUrl.toString()}`);
                    return;
                }
                // Create a deferred promise for the fetch event's response.
                const done = plugins.smartpromise.defer();
                fetchEventArg.respondWith(done.promise);
                // Determine whether this request should be cached.
                if ((originalRequest.method === 'GET' &&
                    originalRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin) &&
                    !originalRequest.url.includes('/api/') &&
                    !originalRequest.url.includes('smartserve/reloadcheck')) ||
                    originalRequest.url.includes('https://assetbroker.') ||
                    originalRequest.url.includes('https://unpkg.com') ||
                    originalRequest.url.includes('https://fonts.googleapis.com') ||
                    originalRequest.url.includes('https://fonts.gstatic.com')) {
                    // Kick off an asynchronous update check.
                    this.losslessServiceWorkerRef.updateManager.checkUpdate(this);
                    const matchRequest = createMatchRequest(originalRequest);
                    const cachedResponse = await caches.match(matchRequest);
                    const metrics = getMetricsCollector();
                    const eventBus = getEventBus();
                    if (cachedResponse) {
                        // Record cache hit
                        const contentLength = cachedResponse.headers.get('content-length');
                        const bytes = contentLength ? parseInt(contentLength, 10) : 0;
                        const contentType = cachedResponse.headers.get('content-type') || 'unknown';
                        metrics.recordCacheHit(matchRequest.url, bytes);
                        metrics.recordResourceAccess(matchRequest.url, true, contentType, bytes);
                        eventBus.emitCacheHit(matchRequest.url, bytes);
                        logger.log('ok', `CACHED: Found cached response for ${matchRequest.url}`);
                        done.resolve(cachedResponse);
                        return;
                    }
                    // Record cache miss
                    metrics.recordCacheMiss(matchRequest.url);
                    eventBus.emitCacheMiss(matchRequest.url);
                    logger.log('info', `NOTYETCACHED: Trying to cache ${matchRequest.url}`);
                    let newResponse;
                    try {
                        // Use deduplicated fetch to prevent concurrent requests for the same resource
                        newResponse = await this.fetchWithDeduplication(matchRequest);
                    }
                    catch (err) {
                        logger.log('error', `Fetch error for ${matchRequest.url}: ${err}`);
                        newResponse = await create500Response(matchRequest, new Response(err.message));
                    }
                    // Check if the response should be cached. In this version, if the response status is >299 or the response is opaque, we do not cache.
                    if (newResponse.status > 299 || newResponse.type === 'opaque') {
                        logger.log('error', `NOTCACHED: Can't cache response for ${matchRequest.url} (status: ${newResponse.status}, type: ${newResponse.type})`);
                        // Optionally, you can force a 500 response so errors are clearly visible.
                        done.resolve(await create500Response(matchRequest, newResponse));
                    }
                    else {
                        try {
                            const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
                            const responseToPutToCache = newResponse.clone();
                            // Create new headers preserving all except caching-related ones.
                            const headers = new Headers();
                            responseToPutToCache.headers.forEach((value, key) => {
                                if (!['Cache-Control', 'cache-control', 'Expires', 'expires', 'Pragma', 'pragma'].includes(key)) {
                                    headers.set(key, value);
                                }
                            });
                            // Ensure that CORS-related headers are present.
                            if (!headers.has('Access-Control-Allow-Origin')) {
                                headers.set('Access-Control-Allow-Origin', '*');
                            }
                            if (!headers.has('Access-Control-Allow-Methods')) {
                                headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
                            }
                            if (!headers.has('Access-Control-Allow-Headers')) {
                                headers.set('Access-Control-Allow-Headers', 'Content-Type');
                            }
                            // Set Cross-Origin-Resource-Policy
                            if (matchRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
                                // For same-origin resources
                                headers.set('Cross-Origin-Resource-Policy', 'same-origin');
                            }
                            else {
                                // For cross-origin resources that we explicitly allow
                                headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
                            }
                            // Set caching headers - use modern Cache-Control only
                            headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
                            // IMPORTANT: Read the full response body as a blob to avoid issues (e.g., Safari locked streams).
                            const bodyBlob = await responseToPutToCache.blob();
                            const newCachedResponse = new Response(bodyBlob, {
                                status: responseToPutToCache.status,
                                statusText: responseToPutToCache.statusText,
                                headers
                            });
                            await cache.put(matchRequest, newCachedResponse);
                            // Record resource access for per-resource tracking
                            const cachedContentType = newResponse.headers.get('content-type') || 'unknown';
                            const cachedSize = bodyBlob.size;
                            metrics.recordResourceAccess(matchRequest.url, false, cachedContentType, cachedSize);
                            logger.log('ok', `NOWCACHED: Cached response for ${matchRequest.url} for subsequent requests!`);
                            done.resolve(newResponse);
                        }
                        catch (err) {
                            logger.log('error', `Error caching response for ${matchRequest.url}: ${err}`);
                            done.resolve(await create500Response(matchRequest, newResponse));
                        }
                    }
                }
                else {
                    // For requests not intended for caching, simply fetch from the origin.
                    logger.log('ok', `NOTCACHED: Not caching ${originalRequest.url}. Fetching from origin...`);
                    try {
                        const originResponse = await fetch(originalRequest);
                        done.resolve(originResponse);
                    }
                    catch (err) {
                        logger.log('error', `Fetch error for ${originalRequest.url}: ${err}`);
                        done.resolve(await create500Response(originalRequest, new Response(err.message)));
                    }
                }
            }
            catch (err) {
                logger.log('error', `Unhandled fetch event error: ${err}`);
            }
        });
    };
    /**
     * Cleans all caches.
     * Should only be run when a new ServiceWorker is activated.
     */
    cleanCaches = async (reasonArg = 'no reason given') => {
        try {
            logger.log('info', `MAJOR CACHEEVENT: Cleaning caches now! Reason: ${reasonArg}`);
            const cacheNames = await caches.keys();
            const deletePromises = cacheNames.map((cacheToDelete) => caches.delete(cacheToDelete).then(() => {
                logger.log('ok', `Deleted cache ${cacheToDelete}`);
            }));
            await Promise.all(deletePromises);
        }
        catch (err) {
            logger.log('error', `Error cleaning caches: ${err}`);
        }
    };
    /**
     * Revalidates the runtime cache by fetching fresh responses and updating the cache.
     */
    async revalidateCache() {
        try {
            const runtimeCache = await caches.open(this.usedCacheNames.runtimeCacheName);
            const cacheKeys = await runtimeCache.keys();
            for (const requestArg of cacheKeys) {
                try {
                    const clonedRequest = requestArg.clone();
                    const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000);
                    if (response && response.status >= 200 && response.status < 300) {
                        await runtimeCache.delete(requestArg);
                        await runtimeCache.put(requestArg, response);
                    }
                }
                catch (err) {
                    logger.log('error', `Error revalidating cache for ${requestArg.url}: ${err}`);
                }
            }
        }
        catch (err) {
            logger.log('error', `Error revalidating runtime cache: ${err}`);
        }
    }
}
//# sourceMappingURL=data:application/json;base64, 