/**
 * Connect Auth JavaScript SDK
 *
 * A programmatic JavaScript SDK for integrating the Connect Auth widget
 * into web applications without requiring HTML custom elements.
 *
 * @packageDocumentation
 */

/**
 * Generic app event structure
 * @template TType - Event type string
 * @template TData - Event data payload
 */
declare type AppEvent<TType extends string = string, TData = Record<string, unknown>> = {
    /** The type of event that occurred */
    type: TType;
    /** Data associated with the event */
    data: TData;
};

/**
 * Auth SDK class for programmatic control of the Connect Auth widget
 *
 * @example
 * ```javascript
 * const auth = new Auth({
 *   jwt: 'your-jwt-token',
 *   env: 'production',
 *   theme: 'dark', // 'auto' | 'light' | 'dark'
 *   onLoaded: () => console.log('Auth loaded and ready'),
 *   onError: ({ error, reason }) => console.error(error, reason),
 *   onClose: () => console.log('Auth closed'),
 *   onDeposit: ({ data }) => console.log('Deposit', data),
 *   onEvent: ({ type, data }) => console.log('Event', type, data)
 * });
 *
 * // Render to a container
 * await auth.render(document.getElementById('auth-container'));
 *
 * // Update configuration
 * auth.updateConfig({ jwt: 'new-token', theme: 'light' });
 *
 * // Clean up
 * auth.destroy();
 * ```
 */
declare class Auth extends BaseJsSdk<AuthConfig> {
    protected errorMessages: {
        readonly ALREADY_RENDERED: "Auth widget is already rendered. Call destroy() before rendering again.";
        readonly NOT_RENDERED: "Auth widget is not rendered. Call render() first.";
        readonly INVALID_CONTAINER: "Invalid container element provided.";
        readonly SCRIPT_LOAD_FAILED: "Failed to load the Connect Auth script.";
        readonly WEB_COMPONENT_NOT_DEFINED: "Web component is not defined. Script may not be loaded.";
    };
    protected scriptUrls: {
        readonly sandbox: "https://sdk.sandbox.connect.xyz/auth-web/index.js";
        readonly production: "https://sdk.connect.xyz/auth-web/index.js";
    };
    protected webComponentTag: string;
    /**
     * Render the Auth widget to a container element
     * @param container - The container element to render the widget into
     * @returns Promise that resolves when the widget is rendered
     */
    render(container: HTMLElement): Promise<void>;
    /**
     * Update the configuration of the Auth widget
     * @param config - Partial configuration to update
     */
    updateConfig(config: Partial<AuthConfig>): void;
    /**
     * Get the current configuration
     * @returns The current configuration object
     */
    getConfig(): Readonly<AuthConfig>;
    /**
     * Check if the Auth widget is currently rendered
     * @returns True if the widget is rendered, false otherwise
     */
    isRendered(): boolean;
    /**
     * Destroy the Auth widget and clean up resources
     */
    destroy(): void;
}
export { Auth }
export default Auth;

/**
 * Callback function types for Connect Auth components.
 * Common ones (incl. `onDeposit`) come from `CommonCallbacks`.
 */
declare type AuthCallbacks = CommonCallbacks<AuthEvent>;

export declare interface AuthConfig extends BaseConfig<AuthEvent>, AuthCallbacks {
}

/**
 * Auth event structure for auth-specific events
 */
declare type AuthEvent = AppEvent<AuthEventType, DepositSubmittedEventData>;

/**
 * Auth event type literals
 */
declare type AuthEventType = 'deposit.submitted';

/**
 * Configuration options for the SDK
 */
declare interface BaseConfig<TEvent = AppEvent> extends CommonCallbacks<TEvent> {
    /**
     * JWT token used for authentication. Provide/update it via `setJWT`
     * (`updateConfig({ jwt })`).
     */
    jwt: string;

    /**
     * Target environment
     * @default 'production'
     */
    env?: Environment;

    /**
     * Theme mode
     * @default 'light'
     *
     * Available themes:
     * - `'auto'` - Automatically detect system preference (light/dark mode)
     * - `'light'` - Force light theme
     * - `'dark'` - Force dark theme
     */
    theme?: Theme;
}

declare type BaseErrorMessageKeys =
| 'ALREADY_RENDERED'
| 'NOT_RENDERED'
| 'INVALID_CONTAINER'
| 'SCRIPT_LOAD_FAILED'
| 'WEB_COMPONENT_NOT_DEFINED';

declare abstract class BaseJsSdk<Config extends BaseConfig<never> = BaseConfig> {
    private config: Config;
    private state: SdkState;
    private scriptLoadingPromise?: Promise<void>;

    protected abstract errorMessages: Record<BaseErrorMessageKeys, string>;
    protected abstract scriptUrls: Record<string, string>;
    protected abstract webComponentTag: string;

    constructor(config: Config) {
        if (!config.jwt || typeof config.jwt !== 'string') {
            throw new Error(INVALID_JWT_ERROR_MESSAGE);
        }

        this.config = {
            ...config,
            env: config.env || DEFAULT_ENVIRONMENT,
            theme: config.theme,
        };

        this.state = {
            initialized: false,
            scriptLoaded: false,
            container: null,
            element: null,
        };
    }

    /**
     * Render the widget to a container element
     * @param container - The container element to render the widget into
     * @returns Promise that resolves when the widget is rendered
     */
    async render(container: HTMLElement): Promise<void> {
        // Validate container
        if (!container || !(container instanceof HTMLElement)) {
            throw new Error(this.errorMessages.INVALID_CONTAINER);
        }

        // Check if already rendered
        if (this.state.initialized) {
            throw new Error(this.errorMessages.ALREADY_RENDERED);
        }

        try {
            // Ensure script is loaded
            await this.ensureScriptLoaded();

            // Create and append the web component
            const element = this.createWebComponent();

            // Clear the container and append the element
            container.innerHTML = '';
            container.appendChild(element);

            // Update state
            this.state.container = container;
            this.state.element = element;
            this.state.initialized = true;
        } catch (error) {
            console.error('Failed to render widget:', error);
            throw error;
        }
    }

    /**
     * Update the configuration of the widget (e.g. `theme`, or the `jwt`).
     *
     * Passing a `jwt` is the JS-SDK equivalent of `setJWT` — it provides the JWT
     * used to authenticate.
     *
     * @param config - Partial configuration to update
     * @returns void
     */
    updateConfig(config: Partial<Config>): void {
        if (!this.state.initialized || !this.state.element) {
            throw new Error(this.errorMessages.NOT_RENDERED);
        }

        const element = this.state.element as SdkElement<Config>;

        Object.entries(config).forEach(([key, value]) => {
            if (value) {
                this.config[key as keyof Config] = value;
                element[key as keyof Config] = value;
            }
        });
    }

    /**
     * Destroy the widget and clean up resources
     */
    destroy(): void {
        if (!this.state.initialized) {
            return; // Nothing to destroy
        }

        // Remove the element from the container
        if (this.state.element && this.state.element.parentNode) {
            this.state.element.parentNode.removeChild(this.state.element);
        }

        // Clear the container
        if (this.state.container) {
            this.state.container.innerHTML = '';
        }

        // Reset state
        this.state.container = null;
        this.state.element = null;
        this.state.initialized = false;
    }

    /**
     * Check if the widget is currently rendered
     * @returns True if the widget is rendered, false otherwise
     */
    isRendered(): boolean {
        return this.state.initialized;
    }

    /**
     * Get the current configuration
     * @returns The current configuration object
     */
    getConfig(): Readonly<Config> {
        return { ...this.config };
    }

    private getEnvironment(): Environment {
        return this.config.env || DEFAULT_ENVIRONMENT;
    }

    private getScriptId() {
        return `${this.webComponentTag}-script-${this.getEnvironment()}`;
    }

    private isScriptLoaded() {
        return !!document.getElementById(this.getScriptId());
    }

    private getWebComponent() {
        return customElements.get(this.webComponentTag);
    }

    private getEffectiveScriptUrls(): Record<string, string> {
        // Support local development with Vite: an internal build pins the script
        // URL for the configured env, bypassing the CDN maps entirely.
        if (typeof import.meta !== 'undefined' && import.meta.env?.['VITE_INTERNAL_BUILD'] === 'true') {
            const override = import.meta.env['VITE_SCRIPT_URL'];
            if (override) return { [this.getEnvironment()]: override };
        }

        return this.scriptUrls;
    }

    private async loadScript() {
        // When the element is already defined or another instance's script tag is
        // in flight, defer to waitForWebComponent — the shared loader would no-op
        // for those cases and never settle this promise.
        if (this.getWebComponent() || this.isScriptLoaded()) {
            return;
        }

        if (this.scriptLoadingPromise) {
            return this.scriptLoadingPromise;
        }

        // The shared loader also reports failures to Faro (with JWT claims for
        // partner/participant context) and removes a failed tag so a later
        // render() call can retry cleanly.
        this.scriptLoadingPromise = new Promise<void>((resolve, reject) => {
            loadWebComponentScript({
                webComponentTag: this.webComponentTag,
                appName: this.webComponentTag,
                scriptUrls: this.getEffectiveScriptUrls(),
                collectorUrls: FARO_COLLECTOR_URLS,
                env: this.getEnvironment(),
                jwt: this.config.jwt,
                onLoad: resolve,
                onError: (info) => {
                    reject(
                    new Error(
                    info.reason === 'not-defined'
                    ? this.errorMessages.WEB_COMPONENT_NOT_DEFINED
                    : `${this.errorMessages.SCRIPT_LOAD_FAILED} (${this.getEnvironment()})`
                    )
                    );
                },
            });
        });

        try {
            await this.scriptLoadingPromise;
        } catch (error) {
            this.scriptLoadingPromise = undefined;
            throw error;
        }

        return this.scriptLoadingPromise;
    }

    private async waitForWebComponent() {
        if (this.getWebComponent()) {
            return;
        }

        // The script has already loaded (loadScript awaited its onLoad); this only
        // covers the window until the custom element registers. Under load (CI, slow
        // devices) that can take longer than a few seconds, so the grace period is
        // generous — a genuine load failure still fails fast via loadScript's
        // onError, not this timer.
        return new Promise<void>((resolve, reject) => {
            const timeoutId = setTimeout(() => {
                reject(new Error(`Timeout waiting for ${this.webComponentTag} to be defined`));
            }, DEFAULT_WEB_COMPONENT_TIMEOUT_MS);

            customElements
            .whenDefined(this.webComponentTag)
            .then(() => {
                clearTimeout(timeoutId);
                resolve();
            })
            .catch((error) => {
                clearTimeout(timeoutId);
                reject(error);
            });
        });
    }

    /**
     * Load the web component script if not already loaded
     */
    protected async ensureScriptLoaded(): Promise<void> {
        if (this.state.scriptLoaded) {
            return;
        }

        try {
            await this.loadScript();
            await this.waitForWebComponent();
            this.state.scriptLoaded = true;
        } catch (error) {
            console.error('Failed to load Connect script:', error);
            throw error;
        }
    }

    private createWebComponent() {
        const element = document.createElement(this.webComponentTag) as SdkElement<Config>;

        Object.entries(this.config).forEach(([key, value]) => {
            if (value) {
                element[key as keyof Config] = value;
            }
        });

        return element;
    }
}

/**
 * Common callback function types shared across all Connect apps
 * @template TEvent - The event type for onEvent callback
 */
declare type CommonCallbacks<TEvent = AppEvent> = {
    /** Called when an error occurs */
    onError?: (error: ErrorPayload) => void;
    /** Called when the component is closed */
    onClose?: () => void;
    /** Called when a general event occurs */
    onEvent?: (event: TEvent) => void;
    /** Called when the widget has loaded and is ready */
    onLoaded?: () => void;
    /**
     * Called when a deposit reaches a terminal state (success/failure/verifying).
     * Optional — only SDKs that drive the integrations deposit flow emit it.
     */
    onDeposit?: (deposit: DepositCompletedPayload) => void;
};

export declare type ConnectAuthElement = SdkElement<AuthConfig>;

/**
 * Deposit completed payload — emitted by SDKs that drive a deposit through
 * the integrations flow. Lives on `CommonCallbacks` because both Auth and
 * any SDK embedding `@zerohash/integrations-flow` (e.g. fund with `useAuth`)
 * resolve through the same deposit-status hook.
 */
declare type DepositCompletedPayload = {
    data: {
        depositId: string;
        status: DepositStatus;
        assetId: string;
        networkId: string;
        amount?: string;
        accountMatchingValidation?: {
            status: 'PENDING' | 'VALID' | 'INVALID' | 'ERROR';
            reason?: string;
        };
    };
};

/**
 * Deposit status object — shared across SDKs that surface deposit completion.
 */
declare type DepositStatus = {
    value: string;
    details: string;
    occurredAt: string;
};

/**
 * Deposit submitted event data
 */
declare type DepositSubmittedEventData = {
    /** Unique identifier for the deposit */
    depositId: string;
};

/**
 * Environment configuration for the SDK
 */
export declare type Environment = 'local' | 'dev' | 'cert' | 'prod' | 'sandbox' | 'production';

/**
 * Generic error codes for all Connect applications
 */
export declare enum ErrorCode {
    /** Network connectivity error */
    NETWORK_ERROR = 'network_error',
    /** Authentication or session expired error */
    AUTH_ERROR = 'auth_error',
    /** Resource not found error */
    NOT_FOUND_ERROR = 'not_found_error',
    /** Validation error from user input */
    VALIDATION_ERROR = 'validation_error',
    /** Server error (5xx) */
    SERVER_ERROR = 'server_error',
    /** Client error (4xx) */
    CLIENT_ERROR = 'client_error',
    /** Unknown or unexpected error */
    UNKNOWN_ERROR = 'unknown_error',
}

/**
 * Generic error payload structure for error callbacks
 */
declare type ErrorPayload = {
    /** Error code indicating the type of error */
    errorCode: ErrorCode;
    /** Human-readable reason for the error */
    reason: string;
};

/**
 * Generic HTMLElement representing a web component with custom config
 */
declare type SdkElement<Config extends BaseConfig<never>> = HTMLElement & Config;

/**
 * Internal state of the SDK
 */
declare interface SdkState {
    /** Whether the SDK is initialized */
    initialized: boolean;
    /** Whether the web component script is loaded */
    scriptLoaded: boolean;
    /** The container element where the widget is rendered */
    container: HTMLElement | null;
    /** The web component element */
    element: HTMLElement | null;
}

/**
 * Theme configuration for the SDK
 *
 * - `"auto"` - Automatically detect system preference (light/dark mode)
 * - `"light"` - Force light theme
 * - `"dark"` - Force dark theme
 */
declare type Theme = 'auto' | 'light' | 'dark';

export { }
