import fetch from "node-fetch";
import { getDatabase } from "./database.js";
import * as crypto from "crypto";
import * as zlib from "zlib";
import { promisify } from "util";
import { serverLogger } from "../utils/serverLogger.js";
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
export class ProxyCache {
    db = null;
    pendingRequests = new Map();
    initialized = false;
    MAX_CACHE_ENTRIES = 1000;
    MAX_CACHE_SIZE_MB = 100;
    cleanupInterval = null;
    constructor(){
        // Defer initialization to ensure database is ready
        setImmediate(()=>this.initCache().catch(()=>{
            // Error already logged in initCache, swallow to prevent unhandled rejection
            }));
    }
    async ensureInitialized() {
        if (this.initialized) return;
        // Wait for database to be available
        try {
            const database = getDatabase();
            await database.waitForInitialization();
            this.db = database.db;
            this.initialized = true;
        } catch (error) {
            serverLogger.error("ProxyCache: Failed to initialize database", {
                error
            });
            throw error;
        }
    }
    async initCache() {
        try {
            await this.ensureInitialized();
            if (!this.db) return;
            // Create tables with retry logic for busy database
            await this.createTablesWithRetry();
            // Start cleanup interval
            this.cleanupInterval = setInterval(async ()=>{
                try {
                    await this.cleanup();
                } catch (error) {
                    serverLogger.error("ProxyCache cleanup error", {
                        error
                    });
                }
            }, 60000); // Every minute
        } catch (error) {
            serverLogger.error("ProxyCache failed to initialize cache", {
                error
            });
        // ProxyCache will work without caching if DB is not available
        }
    }
    async createTablesWithRetry(maxRetries = 10, baseDelay = 1000) {
        if (!this.db) return;
        for(let attempt = 0; attempt < maxRetries; attempt++){
            try {
                // Create cache table with compression support
                this.db.exec(`
        CREATE TABLE IF NOT EXISTS proxy_cache (
          key TEXT PRIMARY KEY,
          data BLOB,
          compressed INTEGER DEFAULT 0,
          expires_at INTEGER,
          created_at INTEGER,
          hit_count INTEGER DEFAULT 0,
          last_hit INTEGER,
          size INTEGER,
          headers TEXT
        )
      `);
                // Create indexes for efficient cleanup and lookup
                this.db.exec(`
        CREATE INDEX IF NOT EXISTS idx_proxy_cache_expires ON proxy_cache(expires_at);
        CREATE INDEX IF NOT EXISTS idx_proxy_cache_hits ON proxy_cache(hit_count DESC);
      `);
                return;
            } catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                if (message.includes("busy") && attempt < maxRetries - 1) {
                    const delay = baseDelay * Math.pow(2, attempt);
                    serverLogger.warn(`ProxyCache: Database busy, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
                    await new Promise((resolve)=>setTimeout(resolve, delay));
                } else {
                    serverLogger.error("ProxyCache: Failed to create tables after retries", {
                        error: message
                    });
                    return;
                }
            }
        }
    }
    getCacheKey(url, options) {
        const keyData = {
            url,
            method: options?.method || "GET",
            body: options?.body,
            headers: options?.headers
        };
        return crypto.createHash("sha256").update(JSON.stringify(keyData)).digest("hex");
    }
    getCacheDuration(url) {
        // Smart cache durations based on endpoint
        if (url.includes("/api/tracks/") || url.includes("/api/albums/")) {
            return 7 * 24 * 60 * 60 * 1000; // 7 days for media metadata
        } else if (url.includes("/api/search")) {
            return 60 * 60 * 1000; // 1 hour for search results
        } else if (url.includes("/api/playlists")) {
            return 5 * 60 * 1000; // 5 minutes for playlists
        } else if (url.includes("/api/recommendations")) {
            return 30 * 60 * 1000; // 30 minutes for recommendations
        }
        return 10 * 60 * 1000; // 10 minutes default
    }
    async get(key) {
        await this.ensureInitialized();
        if (!this.db) return null;
        const entry = this.db.prepare(`
      SELECT * FROM proxy_cache 
      WHERE key = ? AND expires_at > ?
    `).get(key, Date.now());
        if (!entry) {
            return null;
        }
        // Update hit statistics
        this.db.prepare(`
      UPDATE proxy_cache 
      SET hit_count = hit_count + 1, last_hit = ?
      WHERE key = ?
    `).run(Date.now(), key);
        // Decompress if needed
        let data = entry.data;
        if (entry.compressed) {
            data = (await gunzip(Buffer.from(data))).toString();
        }
        return JSON.parse(data);
    }
    async set(key, data, duration) {
        await this.ensureInitialized();
        if (!this.db) return;
        const serialized = JSON.stringify(data);
        let compressed = false;
        let dataToStore = serialized;
        // Compress large responses
        if (serialized.length > 1024) {
            dataToStore = await gzip(Buffer.from(serialized));
            compressed = true;
        }
        const expiresAt = Date.now() + (duration || this.getCacheDuration(""));
        this.db.prepare(`
      INSERT OR REPLACE INTO proxy_cache (
        key, data, compressed, expires_at, created_at, 
        hit_count, last_hit, size
      ) VALUES (?, ?, ?, ?, ?, 0, ?, ?)
    `).run(key, dataToStore, compressed ? 1 : 0, expiresAt, Date.now(), Date.now(), Buffer.byteLength(dataToStore));
    }
    async proxyRequest(url, options = {}) {
        const cacheKey = this.getCacheKey(url, options);
        // Check if we're already fetching this
        if (this.pendingRequests.has(cacheKey)) {
            serverLogger.debug("ProxyCache waiting for pending request", {
                url
            });
            return this.pendingRequests.get(cacheKey);
        }
        // Check cache first
        const cached = await this.get(cacheKey);
        if (cached) {
            serverLogger.debug("ProxyCache cache hit", {
                url
            });
            return cached;
        }
        serverLogger.debug("ProxyCache cache miss, fetching", {
            url
        });
        // Create fetch promise
        const fetchPromise = this.fetchAndCache(url, options, cacheKey);
        this.pendingRequests.set(cacheKey, fetchPromise);
        try {
            const result = await fetchPromise;
            return result;
        } finally{
            this.pendingRequests.delete(cacheKey);
        }
    }
    async fetchAndCache(url, options, cacheKey) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            const data = await response.json();
            // Cache successful responses
            const duration = this.getCacheDuration(url);
            await this.set(cacheKey, data, duration);
            return data;
        } catch (error) {
            serverLogger.error("ProxyCache fetch error", {
                url,
                error
            });
            throw error;
        }
    }
    // Express middleware
    middleware() {
        return async (req, res, next)=>{
            // Only proxy specific API calls
            if (!req.path.startsWith("/api/proxy/")) {
                return next();
            }
            const targetPath = req.path.replace("/api/proxy", "");
            const targetUrl = `${process.env.STREAMLINE_API_URL || "https://api.streamline.club"}${targetPath}`;
            try {
                const options = {
                    method: req.method,
                    headers: {
                        ...req.headers,
                        "X-Proxied-By": "SOUND"
                    }
                };
                if (req.body && Object.keys(req.body).length > 0) {
                    options.body = JSON.stringify(req.body);
                    options.headers["Content-Type"] = "application/json";
                }
                const data = await this.proxyRequest(targetUrl, options);
                res.json(data);
            } catch (error) {
                console.error("[ProxyCache] Middleware error:", error);
                res.status(500).json({
                    error: "Proxy request failed"
                });
            }
        };
    }
    // Cleanup old entries
    async cleanup() {
        await this.ensureInitialized();
        if (!this.db) return;
        // Delete expired entries
        const deleted = this.db.prepare(`
      DELETE FROM proxy_cache
      WHERE expires_at < ?
    `).run(Date.now());
        if (deleted.changes > 0) {
            serverLogger.info("ProxyCache cleaned up expired entries", {
                count: deleted.changes
            });
        }
        // PERFORMANCE: Implement LRU eviction to prevent unbounded cache growth
        // Check cache size and entry count
        const stats = this.db.prepare(`
      SELECT
        COUNT(*) as entry_count,
        SUM(size) as total_size
      FROM proxy_cache
    `).get();
        const maxSizeBytes = this.MAX_CACHE_SIZE_MB * 1024 * 1024;
        // If cache is too large, remove least recently used entries
        if (stats.entry_count > this.MAX_CACHE_ENTRIES || stats.total_size > maxSizeBytes) {
            const entriesToRemove = Math.max(stats.entry_count - this.MAX_CACHE_ENTRIES, Math.floor(stats.entry_count * 0.2));
            const removed = this.db.prepare(`
        DELETE FROM proxy_cache
        WHERE key IN (
          SELECT key FROM proxy_cache
          ORDER BY last_hit ASC
          LIMIT ?
        )
      `).run(entriesToRemove);
            serverLogger.info("ProxyCache LRU eviction", {
                removed: removed.changes,
                reason: stats.entry_count > this.MAX_CACHE_ENTRIES ? "max_entries" : "max_size",
                totalEntries: stats.entry_count,
                totalSizeMB: (stats.total_size / (1024 * 1024)).toFixed(2)
            });
        }
    }
    // Get cache statistics
    async getStats() {
        await this.ensureInitialized();
        if (!this.db) return {
            count: 0,
            totalSize: 0,
            avgHitRate: 0
        };
        const stats = this.db.prepare(`
      SELECT 
        COUNT(*) as total_entries,
        SUM(size) as total_size,
        SUM(hit_count) as total_hits,
        AVG(hit_count) as avg_hits,
        MAX(hit_count) as max_hits
      FROM proxy_cache
    `).get();
        const popularEntries = this.db.prepare(`
      SELECT key, hit_count, size, last_hit
      FROM proxy_cache
      ORDER BY hit_count DESC
      LIMIT 10
    `).all();
        return {
            ...stats || {
                entry_count: 0,
                total_size: 0,
                total_hits: 0,
                avg_hits: 0,
                max_hits: 0
            },
            total_size_mb: ((stats?.total_size || 0) / 1024 / 1024).toFixed(2),
            popular_entries: popularEntries
        };
    }
    // Preload frequently accessed content
    async preloadPopular() {
        const popular = this.db.prepare(`
      SELECT key FROM proxy_cache
      WHERE hit_count > 5
      ORDER BY hit_count DESC
      LIMIT 50
    `).all();
        console.info(`[ProxyCache] Preloading ${popular.length} popular entries`);
        for (const entry of popular){
            await this.get(entry.key);
        }
    }
    /**
   * Shutdown and cleanup
   */ shutdown() {
        if (this.cleanupInterval) {
            clearInterval(this.cleanupInterval);
            this.cleanupInterval = null;
        }
        this.pendingRequests.clear();
        serverLogger.info("ProxyCache shutdown complete");
    }
}
// Export singleton
let cacheInstance = null;
export function getProxyCache() {
    if (!cacheInstance) {
        cacheInstance = new ProxyCache();
    }
    return cacheInstance;
}

//# sourceMappingURL=proxyCache.js.map