/**
 * Zerohash Fund JavaScript SDK
 *
 * A programmatic JavaScript SDK for integrating the Fund 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.
        // Dot access, not brackets: Vite 8 replaces `import.meta.env.KEY` with its
        // value but inlines the whole env object for a bracket lookup.
        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;
};

/**
 * Fund SDK class for programmatic control of the Connect Fund widget.
 *
 * Renders the `zerohash-fund` web component, which handles the iframe
 * and postMessage communication internally.
 *
 * @example
 * ```javascript
 * const fund = new Fund({
 *   jwt: 'your-jwt-token',
 *   env: 'prod',
 *   theme: 'light',
 *   onCompleted: ({ assetSymbol, amount }) => console.log('Deposited', amount, assetSymbol),
 *   onClose: () => console.log('Closed'),
 *   onError: ({ errorCode, reason }) => console.error(errorCode, reason),
 * });
 *
 * // Render to a container
 * await cryptoSell.render(document.getElementById('fund-container'));
 *
 * // Update configuration
 * cryptoSell.updateConfig({ jwt: 'new-token', theme: 'dark' });
 *
 * // Clean up
 * cryptoSell.destroy();
 * ```
 */
declare class Fund extends BaseJsSdk<FundConfig> {
    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 Fund 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 Fund widget
     * @param config - Partial configuration to update
     */
    updateConfig(config: Partial<FundConfig>): void;
    /**
     * Get the current configuration
     * @returns The current configuration object
     */
    getConfig(): Readonly<FundConfig>;
    /**
     * Check if the Fund widget is currently rendered
     * @returns True if the widget is rendered, false otherwise
     */
    isRendered(): boolean;
    /**
     * Destroy the Fund widget and clean up resources
     */
    destroy(): void;
}
export { Fund }
export default Fund;

declare type FundCallbacks = CommonCallbacks<FundEvent> & {
    /** Called when the fund deposit is successfully completed */
    onCompleted?: (data: FundCompletedData) => void;
    /**
     * Called when a deposit transaction reaches a terminal **failed** state (the
     * "Transaction failed" page). Distinct from `onError`, which signals an SDK/
     * request error — `onFailed` carries the failed transaction's details.
     */
    onFailed?: (data: FundCompletedData) => void;
    /** Called when the user taps "Provide info" on the travel-rule pending screen; the host opens the Travel Rule SDK scoped to `depositId`. */
    onProvideInfo?: (data: FundProvideInfoData) => void;
};

declare type FundCompletedData = {
    /** Deposit address for the asset */
    depositAddress?: string;
    /** Network used for the deposit */
    network?: string;
    /** Asset symbol (e.g. 'BTC.BITCOIN') */
    assetSymbol?: string;
    /** Amount to be deposited */
    amount?: string;
    /** Backend transaction id for the deposit */
    transactionId?: string;
    /** Fund id the deposit was credited to */
    fundId?: string;
    /** Notional (fiat) amount of the deposit */
    notionalAmount?: string;
};

export declare interface FundConfig extends BaseConfig<FundEvent>, FundCallbacks {
    /**
     * Runs the SDK in Pay mode.
     *
     * - Uses the `pay-sdk` app id and `app:internal:pay` scope when talking to trade-api.
     * - Signs the `account_funding_pay` terms agreement.
     * - Polls deposit status by `transactionId` (single-deposit endpoint) instead of
     *   by `deposit_address` (list endpoint).
     *
     * @default false
     */
    isPay?: boolean;
}

/**
 * Fund event structure
 */
declare type FundEvent = AppEvent<FundEventType>;

/**
 * Fund event type literals
 */
declare type FundEventType = string;

declare type FundProvideInfoData = {
    depositId: 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 ZerohashFundElement = SdkElement<FundConfig>;

export { }
