"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitHubComponentsStore = void 0;
const error_handler_1 = require("../../../utils/error.handler");
const logger_1 = require("../../../utils/logger");
const platform_utils_1 = require("../platform.utils");
const version_selector_1 = require("../version.selector");
// The github release package name is in format {{component}}-{{version}}
// each release has multiple assets; each asset can be platform specific
// each release has a tag the same as the release name; this can be used to get a specific release
// Uses GITHUB_TOKEN environment variable for authentication if available (higher rate limits)
class GitHubComponentsStore {
    baseUrl = '';
    usePrereleases = false;
    logger = logger_1.Logger.getInstance("github.store");
    cachedReleases = null;
    retryOptions = {
        maxRetries: 9,
        baseDelay: 1000, // Start with 1 second
        maxDelay: 30000, // Maximum 30 seconds
        backoffMultiplier: 2
    };
    constructor(repo = "interopio/iocd-components/releases", usePrereleases = false) {
        this.baseUrl = `https://api.github.com/repos/${repo}`;
        this.usePrereleases = usePrereleases;
    }
    /**
     * Configure retry options for API requests
     */
    setRetryOptions(options) {
        this.retryOptions = { ...this.retryOptions, ...options };
        this.logger.debug(`Updated retry options:`, this.retryOptions);
    }
    /**
     * Clear cached releases to force fresh fetch on next request
     */
    clearCache() {
        this.cachedReleases = null;
        this.logger.debug('Cleared GitHub releases cache');
    }
    /**
     * Fetch with retry logic for handling rate limits and temporary failures
     */
    async fetchWithRetry(url, attempt = 1) {
        try {
            this.logger.debug(`Fetching ${url} (attempt ${attempt}/${this.retryOptions.maxRetries + 1})`);
            // Prepare headers with optional GitHub token authorization
            const headers = {
                'User-Agent': 'iocd-cli'
            };
            const githubToken = process.env['GITHUB_TOKEN'];
            if (githubToken) {
                headers['Authorization'] = `Bearer ${githubToken}`;
                this.logger.debug('Using GitHub token for authentication');
            }
            const response = await fetch(url, { headers });
            // Log current rate limit status for debugging
            const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
            const rateLimitLimit = response.headers.get('x-ratelimit-limit');
            if (rateLimitRemaining && rateLimitLimit) {
                this.logger.debug(`GitHub rate limit: ${rateLimitRemaining}/${rateLimitLimit} remaining`);
            }
            // Handle rate limiting (403) and server errors (5xx)
            if (response.status === 403) {
                const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
                const rateLimitReset = response.headers.get('x-ratelimit-reset');
                if (rateLimitRemaining === '0' && rateLimitReset) {
                    // We've hit the rate limit - wait until reset time
                    const resetTime = new Date(parseInt(rateLimitReset) * 1000);
                    const waitTime = Math.max(resetTime.getTime() - Date.now(), 1000); // Minimum 1 second
                    if (attempt <= this.retryOptions.maxRetries && waitTime <= this.retryOptions.maxDelay) {
                        this.logger.debug(`GitHub rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)}s until reset at ${resetTime.toLocaleTimeString()}`);
                        await this.delay(waitTime);
                        return this.fetchWithRetry(url, attempt + 1);
                    }
                    else if (waitTime > this.retryOptions.maxDelay) {
                        const tokenHelp = githubToken
                            ? ''
                            : '\n\nTo increase your rate limit, set a GITHUB_TOKEN:\n  export GITHUB_TOKEN=your_token\n  (Get a token from https://github.com/settings/tokens)';
                        throw new Error(`GitHub rate limit exceeded. Reset time (${Math.ceil(waitTime / 1000)}s) exceeds maximum wait time (${this.retryOptions.maxDelay / 1000}s). Please try again later.${tokenHelp}`);
                    }
                }
                // Handle other 403 errors (like secondary rate limits) with exponential backoff
                if (attempt <= this.retryOptions.maxRetries) {
                    const delay = this.calculateBackoffDelay(attempt);
                    this.logger.debug(`GitHub API returned 403 (${response.statusText}). This might be a secondary rate limit. Retrying in ${delay}ms...`);
                    await this.delay(delay);
                    return this.fetchWithRetry(url, attempt + 1);
                }
                // If we've exhausted retries, provide helpful error message
                const errorBody = await response.text().catch(() => 'Unable to read error details');
                const tokenHelp = githubToken
                    ? ''
                    : '\n\nGitHub rate limit reached. To increase your limit, set a GITHUB_TOKEN:\n  export GITHUB_TOKEN=your_token\n  (Get a token from https://github.com/settings/tokens)';
                throw new Error(`GitHub API rate limit or access denied (403). ${errorBody}. Try again later or check your network connection.${tokenHelp}`);
            }
            // Handle server errors (5xx) with exponential backoff
            if (response.status >= 500 && response.status < 600) {
                if (attempt <= this.retryOptions.maxRetries) {
                    const delay = this.calculateBackoffDelay(attempt);
                    this.logger.warn(`GitHub API server error ${response.status} (${response.statusText}). Retrying in ${delay}ms...`);
                    await this.delay(delay);
                    return this.fetchWithRetry(url, attempt + 1);
                }
            }
            // Handle other client errors (4xx) - don't retry these
            if (response.status >= 400 && response.status < 500 && response.status !== 403) {
                const errorBody = await response.text().catch(() => 'Unable to read error details');
                throw new Error(`GitHub API client error ${response.status} (${response.statusText}): ${errorBody}`);
            }
            return response;
        }
        catch (error) {
            // Handle network errors and other exceptions
            if (attempt <= this.retryOptions.maxRetries && error instanceof Error && !error.message.includes('GitHub API')) {
                const delay = this.calculateBackoffDelay(attempt);
                this.logger.warn(`Network error: ${error.message}. Retrying in ${delay}ms...`);
                await this.delay(delay);
                return this.fetchWithRetry(url, attempt + 1);
            }
            throw error;
        }
    }
    /**
     * Calculate exponential backoff delay
     */
    calculateBackoffDelay(attempt) {
        const delay = this.retryOptions.baseDelay * Math.pow(this.retryOptions.backoffMultiplier, attempt - 1);
        return Math.min(delay, this.retryOptions.maxDelay);
    }
    /**
     * Sleep for the specified number of milliseconds
     */
    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    async getAll() {
        const allReleases = await this.getAllReleases();
        return this.parseReleases(allReleases);
    }
    async download(name, version) {
        // Find the specific release and asset for this component
        const targetComponent = await this.findComponent(name, version);
        if (!targetComponent) {
            throw new Error(`No component found for ${name}@${version}`);
        }
        this.logger.debug(`Downloading ${targetComponent.name} from ${targetComponent.downloadUrl}`);
        const response = await this.fetchWithRetry(targetComponent.downloadUrl);
        if (!response.ok) {
            throw new Error(`Failed to download asset: ${response.status} ${response.statusText}`);
        }
        // Get the data as buffer
        const buffer = await response.arrayBuffer();
        this.logger.debug(`Downloaded ${name}@${version || 'latest'} (${targetComponent.name}) successfully!`);
        return {
            name: name,
            data: new Uint8Array(buffer),
            filename: targetComponent.filename,
            actualVersion: targetComponent.version
        };
    }
    getInfo() {
        const prereleaseStatus = this.usePrereleases ? "including pre-releases" : "stable releases only";
        const authStatus = process.env['GITHUB_TOKEN'] ? "authenticated" : "unauthenticated";
        const retryInfo = `retries: ${this.retryOptions.maxRetries}, max delay: ${this.retryOptions.maxDelay}ms`;
        return `GitHub Releases Store: ${this.baseUrl} (${prereleaseStatus}, ${authStatus}, ${retryInfo})`;
    }
    /**
     * Find a specific release by component name and version
     */
    async findComponent(componentName, version) {
        // If no version specified, find the latest release for this component
        const allReleases = await this.getAllReleases();
        // filter out by component name
        let componentReleases = allReleases.filter(release => release.tag_name.startsWith(`${componentName}-v`));
        if (componentReleases.length === 0) {
            const prereleaseNote = this.usePrereleases ? "" : " (pre-releases excluded - set usePrereleases: true to include them)";
            throw new Error(`No releases found for component '${componentName}'${prereleaseNote}`);
        }
        let components = this.parseReleases(componentReleases);
        const selectedComponent = (0, version_selector_1.selectVersion)(version, components);
        if (selectedComponent) {
            this.logger.debug(`Selected component: ${selectedComponent.name} ${selectedComponent.fullVersion} for platform ${selectedComponent.platform} ${selectedComponent.arch || ''}`);
            return selectedComponent;
        }
        throw new error_handler_1.CLIError(`Component not found for ${componentName}@${version})`, {
            suggestions: [
                `You are trying to install ${componentName}@${version} - check that the component name and version are correct`,
                `Use iocd components browse" to see available components and versions`
            ]
        });
    }
    /**
     * Get all releases without parsing them into components
     * Will filter out pre-releases if usePrereleases is false
     */
    async getAllReleases() {
        if (this.cachedReleases) {
            this.logger.debug(`Using cached releases from GitHub`);
            return this.cachedReleases;
        }
        this.logger.debug(`Fetching all releases from GitHub...`);
        const allReleases = [];
        let nextUrl = `${this.baseUrl}?per_page=100&page=1`;
        while (nextUrl) {
            const response = await this.fetchWithRetry(nextUrl);
            if (!response.ok) {
                throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
            }
            const releases = await response.json();
            if (releases.length === 0) {
                break;
            }
            allReleases.push(...releases);
            const linkHeader = response.headers.get('link');
            nextUrl = this.getNextPageUrl(linkHeader);
            // Safety limit
            if (allReleases.length > 1000) {
                break;
            }
        }
        // Filter out pre-releases if usePrereleases is false
        const filteredReleases = this.usePrereleases
            ? allReleases
            : allReleases.filter(release => !release.prerelease);
        this.cachedReleases = filteredReleases;
        return filteredReleases;
    }
    /**
     * Extract the next page URL from the Link header
     * Link header format: <https://api.github.com/repos/owner/repo/releases?page=2>; rel="next", <https://api.github.com/repos/owner/repo/releases?page=5>; rel="last"
     */
    getNextPageUrl(linkHeader) {
        if (!linkHeader) {
            return null;
        }
        const links = linkHeader.split(',');
        for (const link of links) {
            const match = link.match(/<([^>]+)>;\s*rel="next"/);
            if (match) {
                return match[1];
            }
        }
        return null;
    }
    /** Parse GitHub release information into component metadata */
    parseReleases(releases) {
        this.logger.debug(`Parsing ${releases.length} releases into components...`);
        const components = [];
        for (const release of releases) {
            try {
                this.logger.debug(`Processing release: ${release.name} (tag: ${release.tag_name})`);
                const { name, baseVersion, version, buildNumber, platform, arch } = platform_utils_1.ComponentPlatformUtils.parseComponentName(release.tag_name);
                for (const asset of release.assets) {
                    this.logger.debug(`  Found asset: ${asset.name}`);
                    const assetInfo = platform_utils_1.ComponentPlatformUtils.parseComponentFilename(asset.name);
                    components.push({
                        name,
                        version: baseVersion,
                        buildNumber: buildNumber,
                        fullVersion: version,
                        platform: assetInfo.platform || platform || platform_utils_1.ComponentPlatformUtils.getCurrentPlatform(),
                        arch: assetInfo.arch || arch || process.arch,
                        downloadUrl: asset.browser_download_url,
                        filename: asset.name,
                        prerelease: release.prerelease
                    });
                }
            }
            catch (err) {
                this.logger.warn(`Skipping release with unrecognized format: ${release.name}. Error: ${err.message}`);
            }
        }
        return components;
    }
}
exports.GitHubComponentsStore = GitHubComponentsStore;
//# sourceMappingURL=github.store.js.map