/**
 * Zerohash Profile JavaScript SDK
 *
 * A programmatic JavaScript SDK for integrating the Profile 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;
};

/**
 * 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;
};

/**
 * 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;
};

/**
 * 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;
};

/**
 * Profile SDK class for programmatic control of the Connect Profile widget.
 *
 * Renders the `zerohash-profile` web component, which handles the iframe
 * and postMessage communication internally.
 *
 * @example
 * ```javascript
 * const profile = new Profile({
 *   jwt: 'your-jwt-token',
 *   env: 'prod',
 *   theme: 'light',
 *   onClose: () => console.log('Closed'),
 *   onError: ({ errorCode, reason }) => console.error(errorCode, reason),
 * });
 *
 * // Render to a container
 * await profile.render(document.getElementById('profile-container'));
 *
 * // Update configuration
 * profile.updateConfig({ jwt: 'new-token', theme: 'dark' });
 *
 * // Clean up
 * profile.destroy();
 * ```
 */
declare class Profile extends BaseJsSdk<ProfileConfig> {
    protected errorMessages: {
        ALREADY_RENDERED: string;
        NOT_RENDERED: string;
        INVALID_CONTAINER: string;
        SCRIPT_LOAD_FAILED: string;
        WEB_COMPONENT_NOT_DEFINED: string;
    };
    protected scriptUrls: Record<string, string>;
    protected webComponentTag: string;
    /**
     * Render the Profile 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 Profile widget
     * @param config - Partial configuration to update
     */
    updateConfig(config: Partial<ProfileConfig>): void;
    /**
     * Get the current configuration
     * @returns The current configuration object
     */
    getConfig(): Readonly<ProfileConfig>;
    /**
     * Check if the Profile widget is currently rendered
     * @returns True if the widget is rendered, false otherwise
     */
    isRendered(): boolean;
    /**
     * Destroy the Profile widget and clean up resources
     */
    destroy(): void;
}
export { Profile }
export default Profile;

declare type ProfileCallbacks = CommonCallbacks<ProfileEvent>;

export declare interface ProfileConfig extends BaseConfig<ProfileEvent>, ProfileCallbacks {
}

declare type ProfileEvent = AppEvent<ProfileEventType>;

declare type ProfileEventType = 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 declare type ZerohashProfileElement = SdkElement<ProfileConfig>;

export { }
