import { browser } from '$app/environment';
import { ae_auth_error } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores';

export let temp_get_blob_percent_completed = 0;
export const get_blob_percent_completed = temp_get_blob_percent_completed;

export const temp_get_object_percent_completed = 0;
export const get_object_percent_completed = temp_get_object_percent_completed;

export const get_object = async function get_object({
    api_cfg = null,
    endpoint = '',
    headers = {},
    params = {},
    data = {},
    timeout = 20000,
    return_meta = false,
    return_blob = false,
    filename = '',
    auto_download = false,
    as_list = false, // Is this still really needed?
    // The task_id value should be a random string that is unique to the task. This is used to identify the task in the message event.
    task_id = crypto.randomUUID(),
    log_lvl = 0,
    retry_count = 5 // Number of retry attempts
}: {
    api_cfg: any;
    endpoint: string;
    headers?: any;
    params?: any;
    data?: any;
    timeout?: number;
    return_meta?: boolean;
    return_blob?: boolean;
    filename?: null | string;
    auto_download?: boolean;
    as_list?: boolean;
    task_id?: string;
    log_lvl?: number;
    retry_count?: number;
}) {
    if (log_lvl) {
        console.log(
            `*** get_object() *** Endpoint: ${endpoint} AE Task ID: ${task_id}`
        );
        console.log('Params:', params);
        if (log_lvl > 1) {
            console.log('Data:', data);
        }
    }

    if (!api_cfg) {
        console.log('No API Config was provided. Returning false.');
        return false;
    }

    // FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
    if (typeof navigator !== 'undefined' && !navigator.onLine) {
        if (log_lvl)
            console.log(
                'get_object: Browser is offline. Failing fast to allow cache fallback.'
            );
        return false;
    }

    if (!filename && params.filename) {
        filename = params.filename;
    }

    const url = new URL(endpoint, api_cfg['base_url']);
    Object.keys(params).forEach((key) =>
        url.searchParams.append(key, params[key])
    );

    // Clean and merge headers without mutating the original api_cfg
    const headers_cleaned: key_val = {};
    const merged_headers = { ...api_cfg['headers'], ...headers };

    // Auto-promote account_id from api_cfg to header if missing
    let account_id = merged_headers['x-account-id'] || api_cfg['account_id'];

    // IMMEDIATE ACCOUNT ID SCAVENGING: Read from localStorage to avoid race conditions
    if (!account_id && typeof localStorage !== 'undefined') {
        try {
            const ae_loc_raw = localStorage.getItem('ae_loc');
            if (ae_loc_raw) {
                const ae_loc_json = JSON.parse(ae_loc_raw);
                if (ae_loc_json.account_id) {
                    account_id = ae_loc_json.account_id;
                }
            }
        } catch (e) {
            // Silently fail on storage read
        }
    }

    if (account_id) {
        merged_headers['x-account-id'] = account_id;
    }

    // Handle "Bootstrap Paradox" for unauthenticated requests
    const bypass_val =
        merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
    const is_valid_bypass =
        bypass_val === 'bypass' ||
        bypass_val === 'Nothing to See Here' ||
        bypass_val === 'direct-download';

    if (is_valid_bypass) {
        if (log_lvl > 1)
            console.log(
                'api_get_object: Valid bypass detected. Stripping account ID context.'
            );
        delete merged_headers['x-account-id'];
        delete merged_headers['x_account_id'];
    } else {
        // If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
        // but DO NOT strip the valid Account ID.
        delete merged_headers['x-no-account-id'];
        delete merged_headers['x_no_account_id'];
    }

    // Standardize all headers to kebab-case and ensure string values
    for (const prop in merged_headers) {
        const prop_cleaned = prop.replaceAll('_', '-');
        let value = merged_headers[prop];
        if (value === null || value === undefined) continue;

        if (typeof value !== 'string') {
            value = JSON.stringify(value);
        }
        headers_cleaned[prop_cleaned] = value;
    }

    // Auto-inject Authorization header if JWT is present but header is missing
    let jwt =
        headers_cleaned['jwt'] ||
        headers_cleaned['JWT'] ||
        api_cfg['jwt'] ||
        api_cfg['headers']?.['jwt'] ||
        api_cfg['headers']?.['JWT'];

    // Final Fallback: Direct check of primary ae_loc key
    if (!jwt && typeof localStorage !== 'undefined') {
        try {
            const raw = localStorage.getItem('ae_loc');
            if (raw) {
                const json = JSON.parse(raw);
                if (json.jwt) jwt = json.jwt;
            }
        } catch (e) {
            // Silently fail on storage read
        }
    }

    if (
        jwt &&
        !headers_cleaned['Authorization'] &&
        !headers_cleaned['authorization']
    ) {
        headers_cleaned['Authorization'] = `Bearer ${jwt}`;
    }

    if (log_lvl > 1) {
        console.log('Final cleaned headers:', headers_cleaned);
    }

    // signal is injected per-attempt inside the retry loop so each retry gets
    // a fresh AbortController with its own independent timeout.
    const fetchOptions: RequestInit = {
        method: 'GET',
        headers: headers_cleaned,
        // Be explicit about CORS behavior and redirect handling to avoid
        // environment-dependent defaults that can cause opaque failures.
        mode: 'cors',
        credentials: 'omit',
        redirect: 'follow',
        cache: 'no-store'
    };

    if (log_lvl > 1) {
        console.log('Fetch options:', fetchOptions);
    }

    let fetch_method: any = fetch;
    if (api_cfg.fetch) {
        if (log_lvl > 1) {
            console.log('Using custom fetch function from api_cfg!!!');
        }
        fetch_method = api_cfg.fetch;
    }

    for (let attempt = 1; attempt <= retry_count; attempt++) {
        // FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
        if (typeof navigator !== 'undefined' && !navigator.onLine) {
            if (log_lvl)
                console.log(
                    `get_object: Browser is offline (attempt ${attempt}). Failing fast to allow cache fallback.`
                );
            return false;
        }

        // Fresh AbortController per attempt — ensures each retry has its own
        // independent timeout. Sharing a single controller across retries leaves
        // retries unprotected once the first attempt's clearTimeout() runs.
        const controller = new AbortController();
        // Track whether THIS helper's timeout fired. AbortError alone is ambiguous:
        // it can mean timeout OR intentional caller abort (navigation/unmount).
        // We only retry timeout-aborts; intentional aborts should fail fast.
        let did_timeout_abort = false;
        const timeoutId = setTimeout(() => {
            did_timeout_abort = true;
            console.warn(`API GET: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
            controller.abort();
        }, timeout);

        try {
            const response = await fetch_method(
                url.toString(),
                { ...fetchOptions, signal: controller.signal }
            ).catch(function (error: any) {
                // SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
                if (
                    error.name === 'AbortError' ||
                    error.message?.includes('aborted') ||
                    error.name === 'TypeError'
                ) {
                    if (log_lvl > 1) {
                        console.log(
                            'API GET: Request was aborted or terminated by browser. This is expected during navigation.',
                            error
                        );
                    }
                    return error; // Return error to be handled below
                }

                console.log(
                    'API GET Object *fetch* request failed in an unexpected way.',
                    error
                );
                return error;
            });
            clearTimeout(timeoutId);

            // Check if we should stop due to abort or network failure.
            if (
                response instanceof Error ||
                (response &&
                    (response.name === 'TypeError' ||
                        response.name === 'AbortError'))
            ) {
                // AbortError can be either timeout or intentional abort.
                // Retry only helper-owned timeout aborts; fail fast on caller abort.
                if (response.name === 'AbortError') {
                    if (did_timeout_abort) {
                        throw new Error(
                            `Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
                        );
                    }
                    return false;
                }

                // TypeError = transient network failure (ERR_NETWORK_CHANGED,
                // ERR_NETWORK_IO_SUSPENDED, hotel/conference WiFi blip, etc.).
                // IMPORTANT: throw here so the retry loop's catch block handles it with
                // backoff. Returning false would bypass retries entirely.
                //
                // WHY THIS WAS BROKEN: The Jan 2026 "offline-first fast-paths" commit
                // (a10accfaa) changed .catch() to return the error as a value instead of
                // not returning (undefined). The undefined path fell through to the
                // `if (!response)` throw which DID retry. The explicit `return error` +
                // this `return false` block silently killed the retry for the most common
                // failure mode on conference/hotel WiFi.
                throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
            }

            if (!response) {
                if (log_lvl > 1) {
                    console.log(
                        'API GET Object: Something went wrong with *fetch* request. Returning false? Throwing an error!'
                    );
                }
                throw new Error(
                    `HTTP fetch request was aborted or failed in an unexpected way! URL = ${url.toString()}`
                ); // This will allow it to retry
                // return false; // This will stop the retries
            }

            if (log_lvl) {
                console.log(
                    `Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
                );
                try {
                    console.log(
                        'Response headers:',
                        Object.fromEntries(response.headers.entries())
                    );
                } catch (e) {
                    /* ignore header read errors */
                }
            }
            if (log_lvl > 1) {
                console.log('Response:', response);
            }

            if (!response.ok) {
                if (response.status === 404) {
                    if (log_lvl) {
                        console.log(
                            'The response was a 404 not found "error". Returning null.'
                        );
                    }
                    return null;
                }

                // FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
                if (
                    response.status === 400 ||
                    response.status === 401 ||
                    response.status === 403 ||
                    response.status === 422
                ) {
                    if (log_lvl)
                        console.error(
                            `API Client Failure (${response.status}). Failing fast.`
                        );

                    if (response.status === 401 || response.status === 403) {
                        console.warn(
                            `AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`,
                            {
                                has_auth: !!headers_cleaned['Authorization'],
                                has_api_key:
                                    !!headers_cleaned['x-aether-api-key'],
                                has_account_id:
                                    !!headers_cleaned['x-account-id'],
                                jwt_preview: jwt
                                    ? `${jwt.slice(0, 8)}...`
                                    : 'MISSING'
                            }
                        );
                        // Signal the root layout to show the session-expired banner.
                        if (browser)
                            ae_auth_error.set({
                                type: 'expired',
                                ts: Date.now()
                            });
                    }

                    // Structured Error Handling (V3): Attempt to get rich error metadata
                    let error_json: any = null;
                    try {
                        error_json = await response.json();
                    } catch (e) {
                        // Not JSON
                    }

                    if (log_lvl)
                        console.log(
                            'The response was not ok. Structured Error Check:',
                            error_json
                        );

                    if (error_json?.meta?.details) {
                        return error_json;
                    }

                    // Fallback for standard FastAPI "detail" errors
                    if (error_json?.detail) {
                        return {
                            meta: {
                                success: false,
                                status_code: response.status,
                                details: {
                                    category: 'validation',
                                    message:
                                        typeof error_json.detail === 'string'
                                            ? error_json.detail
                                            : JSON.stringify(error_json.detail),
                                    raw: error_json.detail
                                }
                            }
                        };
                    }

                    return false;
                }

                throw new Error(`HTTP error! status: ${response.status}`);
            }

            if (!return_blob) {
                const json = await response.json();
                if (log_lvl > 1) {
                    console.log('Response JSON:', json);
                }
                if (!Array.isArray(json.data) && as_list) {
                    return [json.data];
                }
                return json.data || json;
            } else {
                const reader = response.body?.getReader();
                const contentLength = +response.headers.get('Content-Length')!;
                let receivedLength = 0;
                const chunks = [];

                while (true) {
                    const { done, value } = await reader!.read();
                    if (done) break;
                    chunks.push(value);
                    receivedLength += value.length;

                    const percent_completed = Math.round(
                        (receivedLength * 100) / contentLength
                    );
                    if (log_lvl > 1) {
                        console.log(
                            'GET Blob Progress:',
                            percent_completed,
                            'Total:',
                            contentLength,
                            'Loaded:',
                            receivedLength,
                            'Percent Completed',
                            percent_completed
                        );
                    }

                    temp_get_blob_percent_completed = percent_completed;

                    try {
                        if (typeof window !== 'undefined') {
                            window.postMessage(
                                {
                                    type: 'api_download_blob',
                                    status: 'downloading',
                                    task_id: task_id,
                                    endpoint: endpoint,
                                    filename: filename,
                                    size_total: contentLength,
                                    size_loaded: receivedLength,
                                    percent_completed: percent_completed
                                },
                                '*'
                            );
                        }
                    } catch (e) {
                        console.error('Error posting message:', e);
                    }
                }

                const blob = new Blob(chunks);
                if (auto_download) {
                    const downloadUrl = window.URL.createObjectURL(blob);
                    const link = document.createElement('a');
                    link.href = downloadUrl;
                    link.setAttribute('download', filename || 'download');
                    document.body.appendChild(link);
                    link.click();
                    link.remove();
                    return true;
                } else {
                    return blob;
                }
            }
        } catch (error) {
            // Ensure the per-attempt timeout timer is always cancelled on failure.
            clearTimeout(timeoutId);
            console.log(
                `API GET object request *fetch* error on attempt ${attempt}:`,
                error
            );

            if (attempt === retry_count) {
                console.log('Max retry attempts reached. Returning false.');
                return false;
            }

            // Backoff before retrying. Without a delay, rapid retries on a flaky
            // connection accomplish nothing and add noise. Caps at 8s so later
            // attempts don't wait excessively. Gives the network time to recover
            // (ERR_NETWORK_CHANGED is typically a sub-second WiFi roam event).
            const delay_ms = Math.min(2000 * attempt, 8000);
            console.log(`API GET: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
            await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
        }
    }
};
