/**
 * MES API Client
 *
 * Unified API client with timeout, retry, and cancellation support.
 *
 * Usage:
 *   const data = await MesApi.get('/api/wip/summary');
 *   const data = await MesApi.post('/api/query_table', { table_name: 'xxx' });
 *
 *   // With options
 *   const data = await MesApi.get('/api/xxx', {
 *       params: { page: 1 },
 *       timeout: 60000,
 *       retries: 5,
 *       signal: abortController.signal,
 *       silent: true
 *   });
 */
const MesApi = (function() {
    'use strict';

    const DEFAULT_TIMEOUT = 30000;  // 30 seconds
    const DEFAULT_RETRIES = 3;
    const RETRY_DELAYS = [1000, 2000, 4000];  // exponential backoff
    const DEGRADED_CODES = new Set([
        'DB_POOL_EXHAUSTED',
        'CIRCUIT_BREAKER_OPEN',
        'SERVICE_UNAVAILABLE'
    ]);
    const POOL_EXHAUSTED_MAX_RETRIES = 0;  // fail fast to avoid thundering herd
    const CIRCUIT_OPEN_MAX_RETRIES = 1;
    const MIN_DEGRADED_DELAY_MS = 3000;

    let requestCounter = 0;

    function getCsrfToken() {
        const meta = document.querySelector('meta[name=\"csrf-token\"]');
        return meta ? meta.content : '';
    }

    function withCsrfHeaders(headers, method) {
        const normalized = (method || 'GET').toUpperCase();
        const nextHeaders = { ...(headers || {}) };
        if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(normalized)) {
            const token = getCsrfToken();
            if (token && !nextHeaders['X-CSRF-Token']) {
                nextHeaders['X-CSRF-Token'] = token;
            }
        }
        return nextHeaders;
    }

    /**
     * Generate a unique request ID
     */
    function generateRequestId() {
        const id = (++requestCounter).toString(36);
        return `req_${id.padStart(4, '0')}`;
    }

    /**
     * Build URL with query parameters
     */
    function buildUrl(url, params) {
        if (!params || Object.keys(params).length === 0) {
            return url;
        }
        const searchParams = new URLSearchParams();
        for (const [key, value] of Object.entries(params)) {
            if (value !== undefined && value !== null) {
                searchParams.append(key, value);
            }
        }
        const separator = url.includes('?') ? '&' : '?';
        return `${url}${separator}${searchParams.toString()}`;
    }

    /**
     * Check if error is retryable
     */
    function isRetryable(error, response, attempt, maxRetries) {
        if (!error) return false;

        // Network errors are retryable
        if (error && error.name === 'TypeError') {
            return attempt < maxRetries;
        }
        // Timeout is retryable
        if (error && error.name === 'TimeoutError') {
            return attempt < maxRetries;
        }
        // User abort or parse errors should never retry
        if (error.isUserAbort || error.isParseError) {
            return false;
        }

        // Degraded response handling with stricter retry policies
        if (error.errorCode === 'DB_POOL_EXHAUSTED') {
            return attempt < Math.min(maxRetries, POOL_EXHAUSTED_MAX_RETRIES);
        }
        if (error.errorCode === 'CIRCUIT_BREAKER_OPEN') {
            return attempt < Math.min(maxRetries, CIRCUIT_OPEN_MAX_RETRIES);
        }

        // Respect HTTP Retry-After when server explicitly asks for backoff
        if (error.retryAfterSeconds && (response?.status === 429 || response?.status === 503)) {
            return attempt < maxRetries;
        }

        // 5xx errors are retryable
        if (response && response.status >= 500) {
            return attempt < maxRetries;
        }
        // 4xx errors are NOT retryable
        if (response && response.status >= 400 && response.status < 500) {
            return false;
        }
        return attempt < maxRetries;
    }

    function parseRetryAfterSeconds(response, errorData) {
        const headerValue = response?.headers?.get?.('Retry-After');
        if (headerValue) {
            const parsed = Number(headerValue);
            if (!Number.isNaN(parsed) && parsed > 0) {
                return parsed;
            }
        }
        const metaRetry = errorData?.meta?.retry_after_seconds;
        const parsedMeta = Number(metaRetry);
        if (!Number.isNaN(parsedMeta) && parsedMeta > 0) {
            return parsedMeta;
        }
        return null;
    }

    function getErrorCode(errorData) {
        return errorData?.error?.code || errorData?.code || null;
    }

    function getErrorMessage(errorData, fallbackStatus) {
        if (errorData?.error?.message) return errorData.error.message;
        if (errorData?.error && typeof errorData.error === 'string') return errorData.error;
        if (errorData?.message) return errorData.message;
        return `HTTP ${fallbackStatus}`;
    }

    function getRetryDelayMs(error, attempt) {
        const baseDelay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
        const retryAfterMs = error?.retryAfterSeconds ? error.retryAfterSeconds * 1000 : 0;

        if (error?.errorCode && DEGRADED_CODES.has(error.errorCode)) {
            return Math.max(baseDelay, retryAfterMs, MIN_DEGRADED_DELAY_MS);
        }
        if (retryAfterMs > 0) {
            return Math.max(baseDelay, retryAfterMs);
        }
        return baseDelay;
    }

    /**
     * Sleep for a given duration
     */
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Execute fetch with timeout
     */
    async function fetchWithTimeout(url, fetchOptions, timeout, externalSignal) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);

        // Link external signal if provided
        if (externalSignal) {
            if (externalSignal.aborted) {
                controller.abort();
            } else {
                externalSignal.addEventListener('abort', () => controller.abort());
            }
        }

        try {
            const response = await fetch(url, {
                ...fetchOptions,
                signal: controller.signal
            });
            clearTimeout(timeoutId);
            return response;
        } catch (error) {
            clearTimeout(timeoutId);
            // Distinguish between timeout and user abort
            if (error.name === 'AbortError') {
                if (externalSignal && externalSignal.aborted) {
                    error.isUserAbort = true;
                } else {
                    // Timeout
                    const timeoutError = new Error('Request timeout');
                    timeoutError.name = 'TimeoutError';
                    throw timeoutError;
                }
            }
            throw error;
        }
    }

    /**
     * Core request function with retry logic
     */
    async function request(method, url, options = {}) {
        const reqId = generateRequestId();
        const timeout = options.timeout || DEFAULT_TIMEOUT;
        const maxRetries = options.retries !== undefined ? options.retries : DEFAULT_RETRIES;
        const silent = options.silent || false;
        const signal = options.signal;

        const fullUrl = buildUrl(url, options.params);
        const startTime = Date.now();

        console.log(`[MesApi] ${reqId} ${method} ${fullUrl}`);

        const fetchOptions = {
            method: method,
            headers: withCsrfHeaders({
                'Content-Type': 'application/json'
            }, method)
        };

        if (options.body) {
            fetchOptions.body = JSON.stringify(options.body);
        }

        let lastError = null;
        let lastResponse = null;
        let loadingToastId = null;

        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                // Check if already aborted
                if (signal && signal.aborted) {
                    console.log(`[MesApi] ${reqId} ⊘ Aborted`);
                    const abortError = new Error('Request aborted');
                    abortError.name = 'AbortError';
                    abortError.isUserAbort = true;
                    throw abortError;
                }

                const response = await fetchWithTimeout(fullUrl, fetchOptions, timeout, signal);
                lastResponse = response;

                if (response.ok) {
                    const elapsed = Date.now() - startTime;
                    console.log(`[MesApi] ${reqId} ✓ ${response.status} (${elapsed}ms)`);

                    // Dismiss loading toast if showing retry status
                    if (loadingToastId) {
                        Toast.dismiss(loadingToastId);
                    }

                    try {
                        const data = await response.json();
                        return data;
                    } catch (parseError) {
                        // JSON parse error on successful response - don't retry
                        console.error(`[MesApi] ${reqId} ✗ JSON parse failed:`, parseError.message);
                        if (!silent) {
                            Toast.error('回應資料解析失敗，資料量可能過大');
                        }
                        parseError.isParseError = true;
                        throw parseError;
                    }
                }

                // Non-OK response
                const errorData = await response.json().catch(() => ({}));
                const error = new Error(getErrorMessage(errorData, response.status));
                error.status = response.status;
                error.data = errorData;
                error.errorCode = getErrorCode(errorData);
                error.retryAfterSeconds = parseRetryAfterSeconds(response, errorData);

                // 4xx errors - don't retry
                if (response.status >= 400 && response.status < 500) {
                    console.log(`[MesApi] ${reqId} ✗ ${response.status} (no retry)`);
                    if (!silent) {
                        Toast.error(`請求錯誤: ${error.message}`);
                    }
                    throw error;
                }

                // 5xx errors - will retry
                lastError = error;

            } catch (error) {
                // User abort - don't retry, no toast
                if (error.isUserAbort) {
                    console.log(`[MesApi] ${reqId} ⊘ Aborted`);
                    if (loadingToastId) {
                        Toast.dismiss(loadingToastId);
                    }
                    throw error;
                }

                // JSON parse error on successful response - don't retry
                if (error.isParseError) {
                    if (loadingToastId) {
                        Toast.dismiss(loadingToastId);
                    }
                    throw error;
                }

                lastError = error;
            }

            // Check if we should retry
            if (attempt < maxRetries && isRetryable(lastError, lastResponse, attempt, maxRetries)) {
                const delay = getRetryDelayMs(lastError, attempt);
                console.log(`[MesApi] ${reqId} ✗ Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);

                if (!silent) {
                    const retryMsg = `正在重試 (${attempt + 1}/${maxRetries})...`;
                    if (loadingToastId) {
                        Toast.update(loadingToastId, { message: retryMsg });
                    } else {
                        loadingToastId = Toast.loading(retryMsg);
                    }
                }

                await sleep(delay);
            }
        }

        // All retries exhausted
        const elapsed = Date.now() - startTime;
        console.log(`[MesApi] ${reqId} ✗ Failed after ${maxRetries} retries (${elapsed}ms)`);

        // Update or dismiss loading toast, show error with retry button
        if (loadingToastId) {
            Toast.dismiss(loadingToastId);
        }

        if (!silent) {
            const errorMsg = lastError.message || '請求失敗';
            Toast.error(`${errorMsg}`, {
                retry: () => request(method, url, options)
            });
        }

        throw lastError;
    }

    // Public API
    return {
        /**
         * Send a GET request
         * @param {string} url - The URL to request
         * @param {Object} options - Request options
         * @param {Object} options.params - URL query parameters
         * @param {number} options.timeout - Timeout in ms (default: 30000)
         * @param {number} options.retries - Max retries (default: 3)
         * @param {AbortSignal} options.signal - AbortController signal
         * @param {boolean} options.silent - Suppress toast notifications
         * @returns {Promise<any>} Response data
         */
        get: function(url, options = {}) {
            return request('GET', url, options);
        },

        /**
         * Send a POST request
         * @param {string} url - The URL to request
         * @param {Object} data - Request body data
         * @param {Object} options - Request options (same as get)
         * @returns {Promise<any>} Response data
         */
        post: function(url, data, options = {}) {
            return request('POST', url, { ...options, body: data });
        }
    };
})();
