import { GetConfiguration } from '@nitrots/nitro-renderer';
import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api';
import { configFileUrl } from '../../secure-assets';
import flagBr from '../../assets/images/flag_icon/flag_icon_br.png';
import flagDe from '../../assets/images/flag_icon/flag_icon_de.png';
import flagEn from '../../assets/images/flag_icon/flag_icon_en.png';
import flagEs from '../../assets/images/flag_icon/flag_icon_es.png';
import flagFi from '../../assets/images/flag_icon/flag_icon_fi.png';
import flagFr from '../../assets/images/flag_icon/flag_icon_fr.png';
import flagIt from '../../assets/images/flag_icon/flag_icon_it.png';
import flagNl from '../../assets/images/flag_icon/flag_icon_nl.png';
import flagSelected from '../../assets/images/flag_icon/flag_icon_selected.png';
import flagTr from '../../assets/images/flag_icon/flag_icon_tr.png';
import { applyTextTranslationLocale } from '../../hooks/translation/useTranslation';
import { TurnstileWidget } from './TurnstileWidget';

type DialogMode = 'login' | 'register' | 'forgot';
type LoginLocale = { code: string; file: string; label: string; flag: string };

const interpolate = (value: string | null | undefined): string =>
{
    if(!value) return '';

    let output = value;

    try { output = GetConfiguration().interpolate(value) || value; }
    catch {}

    return output.replace(/\$\{([^}]+)\}/g, (_, key: string) =>
    {
        if(key === 'api.url' && typeof (window as any).NitroSecureApiUrl === 'string')
        {
            const secureApiUrl = (window as any).NitroSecureApiUrl.replace(/\/$/, '');

            if(secureApiUrl) return secureApiUrl;
        }

        try
        {
            const configValue = GetConfiguration().getValue<string>(key, '');

            if(configValue) return configValue;
        }
        catch {}

        try
        {
            const configValue = GetConfigurationValue<string>(key, '');

            if(configValue) return configValue;
        }
        catch {}

        return '';
    });
};

const LOCK_KEY = 'nitro.login.lock';
const CHAT_TRANSLATION_SETTINGS_KEY = 'chatTranslationSettings';
const MAX_ATTEMPTS = 5;
const LOCK_WINDOW_MS = 60_000;
const LOCK_DURATION_MS = 2 * 60_000;
const getDefaultLoginImages = (): Record<string, string> =>
{
    const imagesBase = (GetConfigurationValue<string>('images.url', '') || '').replace(/\/$/, '');

    if(!imagesBase.length) return { 'background.colour': '#6eadc8' };

    return {
        background: `${ imagesBase }/reception/background_gradient_apr25.png`,
        'background.colour': '#6eadc8',
        drape: `${ imagesBase }/reception/drape.png`,
        left: `${ imagesBase }/reception/mute_reception_backdrop_left.png`,
        right: `${ imagesBase }/reception/background_right.png`
    };
};
const LOGIN_LOCALES: LoginLocale[] = [
    { code: 'it', file: 'it', label: 'Italiano', flag: flagIt },
    { code: 'en', file: 'com', label: 'English', flag: flagEn },
    { code: 'es', file: 'es', label: 'Español', flag: flagEs },
    { code: 'fr', file: 'fr', label: 'Français', flag: flagFr },
    { code: 'de', file: 'de', label: 'Deutsch', flag: flagDe },
    { code: 'pt-BR', file: 'br', label: 'Português', flag: flagBr },
    { code: 'nl', file: 'nl', label: 'Nederlands', flag: flagNl },
    { code: 'fi', file: 'fi', label: 'Suomi', flag: flagFi },
    { code: 'tr', file: 'tr', label: 'Türkçe', flag: flagTr }
];

type AttemptState = { attempts: number; firstAt: number; lockedUntil: number };

const readLock = (): AttemptState =>
{
    try
    {
        const raw = sessionStorage.getItem(LOCK_KEY);
        if(!raw) return { attempts: 0, firstAt: 0, lockedUntil: 0 };
        return JSON.parse(raw);
    }
    catch { return { attempts: 0, firstAt: 0, lockedUntil: 0 }; }
};

const writeLock = (state: AttemptState) =>
{
    try { sessionStorage.setItem(LOCK_KEY, JSON.stringify(state)); }
    catch { }
};

const normalizeLanguageCode = (value: string): string =>
{
    if(!value) return '';

    const normalized = value.trim().replace('_', '-');
    const parts = normalized.split('-');

    if(parts.length === 1) return parts[0].toLowerCase();

    return `${ parts[0].toLowerCase() }-${ parts[1].toUpperCase() }`;
};

const resolveLoginLocale = (value: string): LoginLocale =>
{
    const normalized = normalizeLanguageCode(value);
    const exactMatch = LOGIN_LOCALES.find(locale => normalizeLanguageCode(locale.code) === normalized);

    if(exactMatch) return exactMatch;

    const base = normalized.split('-')[0];

    if(base === 'pt') return LOGIN_LOCALES.find(locale => locale.file === 'br') || LOGIN_LOCALES[0];

    return LOGIN_LOCALES.find(locale => normalizeLanguageCode(locale.code).split('-')[0] === base) || LOGIN_LOCALES[0];
};

const getBrowserLocale = (): LoginLocale =>
{
    if(typeof navigator === 'undefined') return LOGIN_LOCALES[0];

    return resolveLoginLocale(navigator.language || navigator.languages?.[0] || 'it');
};

const readCachedLocale = (): LoginLocale =>
{
    try
    {
        const settings = JSON.parse(localStorage.getItem(CHAT_TRANSLATION_SETTINGS_KEY) || '{}');

        if(typeof settings.uiTextLanguage === 'string' && settings.uiTextLanguage.length) return resolveLoginLocale(settings.uiTextLanguage);
    }
    catch {}

    return getBrowserLocale();
};

const applyLocaleSelection = (locale: LoginLocale): void =>
{
    try
    {
        const previousSettings = JSON.parse(localStorage.getItem(CHAT_TRANSLATION_SETTINGS_KEY) || '{}');
        const nextSettings = {
            enabled: previousSettings.enabled ?? false,
            incomingTargetLanguage: previousSettings.incomingTargetLanguage || locale.code,
            outgoingTargetLanguage: previousSettings.outgoingTargetLanguage || locale.code,
            ...previousSettings,
            uiTextLanguage: locale.code
        };

        localStorage.setItem(CHAT_TRANSLATION_SETTINGS_KEY, JSON.stringify(nextSettings));
    }
    catch {}
};

export interface LoginViewProps
{
    onAuthenticated: (ssoTicket: string) => void;
    isEntering?: boolean;
}

export const LoginView: FC<LoginViewProps> = ({ onAuthenticated, isEntering = false }) =>
{
    const [ mode, setMode ] = useState<DialogMode>('login');
    const [ username, setUsername ] = useState('');
    const [ password, setPassword ] = useState('');
    const [ error, setError ] = useState<string | null>(null);
    const [ info, setInfo ] = useState<string | null>(null);
    const [ submitting, setSubmitting ] = useState(false);
    const [ loginTurnstileToken, setLoginTurnstileToken ] = useState('');
    const [ loginTurnstileResetSignal, setLoginTurnstileResetSignal ] = useState(0);
    const [ loginServerReachable, setLoginServerReachable ] = useState<boolean | null>(null);
    const [ loginPingingServer, setLoginPingingServer ] = useState(false);
    const [ rememberMe, setRememberMe ] = useState(() => !!GetRememberLogin());
    const [ selectedLocale, setSelectedLocale ] = useState<LoginLocale>(() => readCachedLocale());
    const [ localeApplying, setLocaleApplying ] = useState(false);
    const [ localeError, setLocaleError ] = useState('');
    const [ loginViewConfig, setLoginViewConfig ] = useState<Record<string, unknown>>(() => GetConfigurationValue<Record<string, unknown>>('loginview', {}));
    const submitTimeRef = useRef(0);

    const configuredLoginImages: Record<string, string> = (loginViewConfig?.['images'] as Record<string, string>) ?? {};
    const loginImages: Record<string, string> = { ...getDefaultLoginImages(), ...configuredLoginImages };
    
    const configuredLoginWidgets: Record<string, unknown> = (loginViewConfig?.['widgets'] as Record<string, unknown>) ?? {};
    const loginWidgetSlots = useMemo(() =>
    {
        return Object.entries(configuredLoginWidgets)
            .filter(([ key, value ]) => key.startsWith('slot.') && key.endsWith('.widget') && typeof value === 'string' && value.length > 0)
            .map(([ key, value ]) =>
            {
                const slotNum = key.match(/\d+/)?.[0] ?? '';
                const conf = configuredLoginWidgets[`slot.${ slotNum }.conf`] as Record<string, unknown> ?? {};

                return { key, slotNum: Number(slotNum), type: value as string, conf };
            })
            .filter(slot => slot.slotNum > 0)
            .sort((a, b) => a.slotNum - b.slotNum);
    }, [ configuredLoginWidgets ]);

    const backgroundColor = (loginImages['background.colour'] || GetConfigurationValue<string>('login_background.colour', '#6eadc8'));
    const background = interpolate(loginImages['background'] || GetConfigurationValue<string>('login_background', ''));
    const sun = interpolate(loginImages['sun'] || GetConfigurationValue<string>('login_sun', ''));
    const drape = interpolate(loginImages['drape'] || GetConfigurationValue<string>('login_drape', ''));
    const left = interpolate(loginImages['left'] || GetConfigurationValue<string>('login_left', ''));
    const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue<string>('login_right.repeat', ''));
    const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
    const widgetImageUrls = useMemo(() => loginWidgetSlots
        .map(slot => typeof slot.conf.image === 'string' ? interpolate(slot.conf.image) : '')
        .filter(Boolean), [ loginWidgetSlots ]);
    const loginImageUrls = useMemo(() => [ background, sun, drape, left, rightRepeat, right, ...widgetImageUrls ].filter(Boolean), [ background, sun, drape, left, rightRepeat, right, widgetImageUrls ]);
    const [ loginImagesVersion, setLoginImagesVersion ] = useState(0);
    const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
    const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
    const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
    const turnstileSiteKey = GetConfigurationValue<string>('login.turnstile.sitekey', '');
    const rawTurnstileEnabled = GetConfigurationValue<unknown>('login.turnstile.enabled', false);
    const turnstileEnabled = (rawTurnstileEnabled === true
        || rawTurnstileEnabled === 'true'
        || rawTurnstileEnabled === 1
        || rawTurnstileEnabled === '1') && !!turnstileSiteKey;

    const resetLoginTurnstile = useCallback(() =>
    {
        setLoginTurnstileToken('');
        setLoginTurnstileResetSignal(prev => prev + 1);
    }, []);

    useEffect(() =>
    {
        setError(null);
        if(mode === 'login') resetLoginTurnstile();
    }, [ mode, resetLoginTurnstile ]);

    useEffect(() =>
    {
        let cancelled = false;

        const refreshLoginViewConfig = () =>
        {
            if(cancelled) return;

            const nextConfig = GetConfigurationValue<Record<string, unknown>>('loginview', {});

            setLoginViewConfig(previousConfig =>
            {
                try
                {
                    return JSON.stringify(previousConfig) === JSON.stringify(nextConfig) ? previousConfig : nextConfig;
                }
                catch
                {
                    return nextConfig;
                }
            });
        };

        refreshLoginViewConfig();

        const timers = [ 50, 150, 300, 600, 1000, 2000 ].map(delay => window.setTimeout(refreshLoginViewConfig, delay));

        return () =>
        {
            cancelled = true;
            timers.forEach(timer => window.clearTimeout(timer));
        };
    }, []);

    const confirmLocaleSelection = useCallback(async () =>
    {
        if(localeApplying) return;

        setLocaleApplying(true);
        setLocaleError('');

        try
        {
            applyLocaleSelection(selectedLocale);
            await applyTextTranslationLocale(selectedLocale.code);
        }
        catch
        {
            setLocaleError('Unable to load this language pack.');
        }
        finally
        {
            setLocaleApplying(false);
        }
    }, [ localeApplying, selectedLocale ]);

    useEffect(() =>
    {
        if(!loginImageUrls.length) return;

        let cancelled = false;

        loginImageUrls.forEach(url =>
        {
            const image = new Image();

            image.onload = image.onerror = () =>
            {
                if(!cancelled) setLoginImagesVersion(version => version + 1);
            };

            image.src = url;
        });

        return () =>
        {
            cancelled = true;
        };
    }, [ loginImageUrls ]);

    useEffect(() =>
    {
        if(!info) return;
        const timeout = window.setTimeout(() => setInfo(null), 8000);
        return () => window.clearTimeout(timeout);
    }, [ info ]);

    const lockState = useMemo(() => readLock(), [ submitting ]);
    const now = Date.now();
    const isLocked = lockState.lockedUntil > now;

    const recordFailure = useCallback(() =>
    {
        const state = readLock();
        const currentNow = Date.now();

        if(currentNow - state.firstAt > LOCK_WINDOW_MS)
        {
            writeLock({ attempts: 1, firstAt: currentNow, lockedUntil: 0 });
            return;
        }

        const attempts = state.attempts + 1;
        const lockedUntil = attempts >= MAX_ATTEMPTS ? currentNow + LOCK_DURATION_MS : 0;
        writeLock({ attempts, firstAt: state.firstAt || currentNow, lockedUntil });
    }, []);

    const clearLock = useCallback(() =>
    {
        writeLock({ attempts: 0, firstAt: 0, lockedUntil: 0 });
    }, []);

    const postJson = useCallback(async (url: string, body: Record<string, unknown>) =>
    {
        const response = await fetch(url, {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'X-Requested-With': 'NitroLoginView'
            },
            body: JSON.stringify(body)
        });

        let payload: Record<string, unknown> = {};
        try { payload = await response.json(); }
        catch { }

        return { ok: response.ok, status: response.status, payload };
    }, []);

    const healthUrl = GetConfigurationValue<string>('login.health.endpoint', '');
    const healthMethodRaw = GetConfigurationValue<string>('login.health.method', 'GET');
    const healthMethod = (healthMethodRaw || 'GET').toUpperCase();
    const checkServerReachable = useCallback(async (): Promise<boolean> =>
    {
        if(!healthUrl) return true;
        try
        {
            const controller = new AbortController();
            const timer = window.setTimeout(() => controller.abort(), 5000);
            try
            {
                const response = await fetch(healthUrl, { method: healthMethod, credentials: 'omit', signal: controller.signal });
                if(response.status === 403) return false;
                if(response.status >= 500) return false;
                return true;
            }
            finally
            {
                window.clearTimeout(timer);
            }
        }
        catch
        {
            return false;
        }
    }, [ healthUrl, healthMethod ]);

    const pingLoginServer = useCallback(async () =>
    {
        setLoginPingingServer(true);
        try
        {
            const ok = await checkServerReachable();
            setLoginServerReachable(ok);
            return ok;
        }
        finally
        {
            setLoginPingingServer(false);
        }
    }, [ checkServerReachable ]);

    const handleLoginSubmit = useCallback(async (event: FormEvent<HTMLFormElement>) =>
    {
        event.preventDefault();

        if(submitting || isEntering) return;

        const nowTs = Date.now();
        if(nowTs - submitTimeRef.current < 1000) return;
        submitTimeRef.current = nowTs;

        const state = readLock();
        if(state.lockedUntil > nowTs)
        {
            const remaining = Math.ceil((state.lockedUntil - nowTs) / 1000);
            setError(`Too many attempts. Try again in ${ remaining }s.`);
            return;
        }

        if(!username.trim() || !password)
        {
            setError('Please enter both your Habbo name and password.');
            return;
        }

        if(turnstileEnabled && !loginTurnstileToken)
        {
            setError('Please complete the security check.');
            return;
        }

        setError(null);
        setSubmitting(true);

        try
        {
            const { ok, payload } = await postJson(loginUrl, {
                username: username.trim(),
                password,
                remember: rememberMe,
                turnstileToken: turnstileEnabled ? loginTurnstileToken : undefined
            });

            const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');

            if(ok && ssoTicket)
            {
                clearLock();
                if(rememberMe) StoreRememberLoginFromPayload(payload, typeof payload.username === 'string' ? payload.username : username.trim(), ssoTicket);
                else ClearRememberLogin();
                onAuthenticated(ssoTicket);
                return;
            }

            recordFailure();
            const message = typeof payload.error === 'string' ? payload.error : 'Invalid Habbo name or password.';
            setError(message);
            resetLoginTurnstile();
        }
        catch(err)
        {
            recordFailure();
            setError('Unable to reach the login service. Please try again.');
            resetLoginTurnstile();
        }
        finally
        {
            setSubmitting(false);
        }
    }, [ submitting, isEntering, username, password, rememberMe, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]);

    const checkEmailUrl = GetConfigurationValue<string>('login.check-email.endpoint', '/api/auth/check-email');
    const checkUsernameUrl = GetConfigurationValue<string>('login.check-username.endpoint', '/api/auth/check-username');
    const imagingUrl = GetConfigurationValue<string>('login.register.imaging.url', '');
    const interpretAvailability = (ok: boolean, status: number, payload: Record<string, unknown>): { available: boolean; error?: string } =>
    {
        const isTrue = (v: unknown) => v === true || v === 'true' || v === 1 || v === '1';
        const isFalse = (v: unknown) => v === false || v === 'false' || v === 0 || v === '0';

        if(ok)
        {
            if(isTrue(payload.available) || isFalse(payload.exists) || isFalse(payload.taken) || isFalse(payload.inUse) || isFalse(payload.in_use)) return { available: true };
            if(isFalse(payload.available) || isTrue(payload.exists) || isTrue(payload.taken) || isTrue(payload.inUse) || isTrue(payload.in_use)) return { available: false, error: typeof payload.error === 'string' ? payload.error : undefined };
            return { available: true };
        }

        if(status === 404 || status === 405 || status === 501) return { available: true };
        if(status === 409) return { available: false, error: typeof payload.error === 'string' ? payload.error : undefined };

        return { available: true };
    };

    const checkEmailAvailable = useCallback(async (email: string): Promise<{ available: boolean; error?: string }> =>
    {
        try
        {
            const { ok, status, payload } = await postJson(checkEmailUrl, { email });
            const result = interpretAvailability(ok, status, payload);
            if(result.available) return { available: true };
            return { available: false, error: result.error || 'This email is already in use.' };
        }
        catch
        {
            return { available: true };
        }
    }, [ checkEmailUrl, postJson ]);

    const checkUsernameAvailable = useCallback(async (username: string): Promise<{ available: boolean; error?: string }> =>
    {
        try
        {
            const { ok, status, payload } = await postJson(checkUsernameUrl, { username });
            const result = interpretAvailability(ok, status, payload);
            if(result.available) return { available: true };
            return { available: false, error: result.error || 'This Habbo name is already taken.' };
        }
        catch
        {
            return { available: true };
        }
    }, [ checkUsernameUrl, postJson ]);

    const handleRegisterSubmit = useCallback(async (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; }, onDialogReset: () => void) =>
    {
        if(turnstileEnabled && !body.turnstileToken)
        {
            setError('Please complete the security check.');
            return;
        }

        setError(null);
        setInfo(null);
        setSubmitting(true);

        try
        {
            const { ok, payload } = await postJson(registerUrl, {
                username: body.username,
                email: body.email,
                password: body.password,
                figure: body.figure,
                gender: body.gender,
                turnstileToken: turnstileEnabled ? body.turnstileToken : undefined
            });

            if(ok)
            {
                const friendly = `Welcome aboard, ${ body.username }! Your account is ready — log in below with the password you just chose.`;
                setInfo(typeof payload.message === 'string' ? payload.message : friendly);
                setMode('login');
                setUsername(body.username);
                setPassword('');
                return;
            }

            setError(typeof payload.error === 'string' ? payload.error : 'Unable to create your account.');
            onDialogReset();
        }
        catch
        {
            setError('Unable to reach the registration service.');
            onDialogReset();
        }
        finally
        {
            setSubmitting(false);
        }
    }, [ turnstileEnabled, registerUrl, postJson ]);

    const handleForgotSubmit = useCallback(async (body: { email: string; turnstileToken: string; }, onDialogReset: () => void) =>
    {
        if(turnstileEnabled && !body.turnstileToken)
        {
            setError('Please complete the security check.');
            return;
        }

        setError(null);
        setInfo(null);
        setSubmitting(true);

        try
        {
            const { ok, payload } = await postJson(forgotUrl, {
                email: body.email,
                turnstileToken: turnstileEnabled ? body.turnstileToken : undefined
            });

            if(ok)
            {
                const friendly = 'Email sent! If an account matches that address you\'ll find a reset link in your inbox shortly (check spam if it doesn\'t show up within a minute).';
                setInfo(typeof payload.message === 'string' ? payload.message : friendly);
                setMode('login');
                return;
            }

            setError(typeof payload.error === 'string' ? payload.error : 'Unable to send a reset email right now.');
            onDialogReset();
        }
        catch
        {
            setError('Unable to reach the password reset service.');
            onDialogReset();
        }
        finally
        {
            setSubmitting(false);
        }
    }, [ turnstileEnabled, forgotUrl, postJson ]);

    return (
        <div
            className="nitro-login-view"
            style={ backgroundColor ? { background: backgroundColor } : undefined }
        >
            { background ? <img className="login-background login-layer login-layer-img" src={ background } alt="" draggable={ false } /> : null }
            { sun ? <img className="login-sun login-layer login-layer-img" src={ sun } alt="" draggable={ false } /> : null }
            { drape ? <img className="login-drape login-layer login-layer-img" src={ drape } alt="" draggable={ false } /> : null }
            { left ? <img className="login-left login-layer login-layer-img" src={ left } alt="" draggable={ false } /> : null }
            { rightRepeat ? <div className="login-right-repeat login-layer" style={ { backgroundImage: `url(${ rightRepeat })` } } /> : null }
            { right ? <img className="login-right login-layer login-layer-img" src={ right } alt="" draggable={ false } /> : null }
            <div className="login-image-preloader" aria-hidden="true" data-version={ loginImagesVersion }>
                { loginImageUrls.map(url => <img key={ url } src={ url } decoding="async" loading="eager" alt="" />) }
            </div>

            { loginWidgetSlots.length > 0 &&
                <div className="login-widgets">
                    { loginWidgetSlots.map(slot =>
                    {
                        const image = typeof slot.conf.image === 'string' ? interpolate(slot.conf.image) : '';
                        const texts = typeof slot.conf.texts === 'string' ? slot.conf.texts : '';
                        const btnText = typeof slot.conf.btnText === 'string' ? slot.conf.btnText : '';
                        const btnLink = typeof slot.conf.btnLink === 'string' ? interpolate(slot.conf.btnLink) : '';
                        const title = typeof slot.conf.title === 'string' ? slot.conf.title : (texts || slot.type);
                        const description = typeof slot.conf.description === 'string' ? slot.conf.description : '';

                        return (
                            <div key={ slot.key } className="login-widget-slot" data-widget-type={ slot.type }>
                                { image && <img className="login-widget-image" src={ image } alt="" draggable={ false } /> }
                                <div className="login-widget-content">
                                    <div className="login-widget-title">{ title }</div>
                                    { description && <div className="login-widget-description">{ description }</div> }
                                    { btnText &&
                                        <button
                                            type="button"
                                            className="login-widget-button"
                                            onClick={ () => { if(btnLink) window.location.href = btnLink; } }
                                        >
                                            { btnText }
                                        </button> }
                                </div>
                            </div>
                        );
                    }) }
                </div> }

            <div className="login-stack">
                <div className="nitro-login-card login-language-card">
                    <div className="card-title">Choose your language</div>
                    <div className="login-language-grid" role="list" aria-label="Language selection">
                        { LOGIN_LOCALES.map(locale =>
                            <button
                                key={ locale.code }
                                type="button"
                                className={ `login-language-option ${ selectedLocale.code === locale.code ? 'selected' : '' }` }
                                onClick={ () => setSelectedLocale(locale) }
                                title={ locale.label }
                                aria-label={ locale.label }
                                style={ selectedLocale.code === locale.code ? { backgroundImage: `url(${ flagSelected })` } : undefined }
                            >
                                <img src={ locale.flag } alt="" draggable={ false } />
                                <span>{ locale.label }</span>
                            </button>) }
                    </div>
                    { localeError.length > 0 && <div className="language-error">{ localeError }</div> }
                    <button type="button" className="ok-button login-language-confirm" disabled={ localeApplying } onClick={ confirmLocaleSelection }>
                        { localeApplying ? 'Loading...' : 'OK' }
                    </button>
                </div>

                <div className="nitro-login-card">
                    <div className="card-title">First time here?</div>
                    <div className="card-body register-card-body">
                        <span>Don't have a Habbo yet?</span>
                        <a onClick={ () => setMode('register') }>You can create one here</a>
                    </div>
                </div>

                <div className="nitro-login-card">
                    <div className="card-title">What's your Habbo called?</div>
                    <form className="card-body" onSubmit={ handleLoginSubmit } autoComplete="on">
                        <div className="field">
                            <label htmlFor="login-username">Name of your Habbo</label>
                            <input
                                id="login-username"
                                name="username"
                                autoComplete="username"
                                type="text"
                                maxLength={ 32 }
                                value={ username }
                                onChange={ e => setUsername(e.target.value) }
                            />
                        </div>
                        <div className="field">
                            <label htmlFor="login-password">Password</label>
                            <input
                                id="login-password"
                                name="password"
                                autoComplete="current-password"
                                type="password"
                                maxLength={ 128 }
                                value={ password }
                                onChange={ e => setPassword(e.target.value) }
                            />
                        </div>
                        <label className="remember-row">
                            <input
                                type="checkbox"
                                checked={ rememberMe }
                                onChange={ e => setRememberMe(e.target.checked) }
                            />
                            <span>Ricordami</span>
                        </label>
                        { turnstileEnabled && mode === 'login' &&
                            <TurnstileWidget
                                siteKey={ turnstileSiteKey }
                                size="compact"
                                onToken={ setLoginTurnstileToken }
                                onExpire={ () => setLoginTurnstileToken('') }
                                onError={ () => setLoginTurnstileToken('') }
                                resetSignal={ loginTurnstileResetSignal }
                            /> }
                        { loginServerReachable === false &&
                            <div className="error-line server-offline">
                                The gameserver isn't running right now. Please try again in a moment.
                                <button type="button" className="retry-link" onClick={ pingLoginServer } disabled={ loginPingingServer }>
                                    { loginPingingServer ? 'Checking…' : 'Retry' }
                                </button>
                            </div>
                        }
                        { error && <div className="error-line">{ error }</div> }
                        { info && <div className="info-line">{ info }</div> }
                        <div className="submit-row">
                            <button
                                type="submit"
                                className="ok-button"
                                disabled={ submitting || isEntering || isLocked }
                            >{ isEntering ? 'Entrando…' : loginPingingServer ? 'Checking…' : 'OK' }</button>
                        </div>
                        <a className="forgot" onClick={ () => setMode('forgot') }>Forgotten your password?</a>
                    </form>
                </div>

            </div>

            { mode === 'register' &&
                <RegisterDialog
                    onCancel={ () => setMode('login') }
                    onSubmit={ handleRegisterSubmit }
                    onCheckEmail={ checkEmailAvailable }
                    onCheckUsername={ checkUsernameAvailable }
                    onCheckServer={ checkServerReachable }
                    imagingUrl={ imagingUrl }
                    submitting={ submitting }
                    error={ error }
                    info={ info }
                    turnstileEnabled={ turnstileEnabled }
                    turnstileSiteKey={ turnstileSiteKey }
                /> }

            { mode === 'forgot' &&
                <ForgotDialog
                    onCancel={ () => setMode('login') }
                    onSubmit={ handleForgotSubmit }
                    submitting={ submitting }
                    error={ error }
                    info={ info }
                    turnstileEnabled={ turnstileEnabled }
                    turnstileSiteKey={ turnstileSiteKey }
                /> }
        </div>
    );
};

interface DialogSharedProps
{
    onCancel: () => void;
    submitting: boolean;
    error: string | null;
    info: string | null;
    turnstileEnabled: boolean;
    turnstileSiteKey: string;
}

interface RegisterDialogProps extends DialogSharedProps
{
    onSubmit: (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; }, onDialogReset: () => void) => void;
    onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>;
    onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>;
    onCheckServer: () => Promise<boolean>;
    imagingUrl: string;
}

type RegisterStep = 'credentials' | 'avatar';

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

type GenderKey = 'M' | 'F';

const PART_ROWS: string[] = [ 'hr', 'hd', 'ch', 'lg', 'sh' ];

const FALLBACK_DEFAULTS: Record<GenderKey, Record<string, { partId: number; colors: number[] }>> = {
    M: {
        hr: { partId: 180, colors: [ 45 ] },
        hd: { partId: 180, colors: [ 1 ] },
        ch: { partId: 215, colors: [ 66 ] },
        lg: { partId: 270, colors: [ 82 ] },
        sh: { partId: 290, colors: [ 80 ] }
    },
    F: {
        hr: { partId: 515, colors: [ 45 ] },
        hd: { partId: 600, colors: [ 1 ] },
        ch: { partId: 660, colors: [ 100 ] },
        lg: { partId: 716, colors: [ 82 ] },
        sh: { partId: 725, colors: [ 61 ] }
    }
};

const FALLBACK_HEX: Record<number, string> = {
    1: '#ffcb98', 8: '#f4ac54', 14: '#f5da88', 19: '#b87560', 20: '#9c543f',
    45: '#e8c498', 61: '#f1ece3', 66: '#96743d', 80: '#4f4d4d', 82: '#7f4f30',
    92: '#ececec', 100: '#c7ddff', 106: '#c6e6bd', 110: '#91a7c8', 143: '#ffffff'
};

interface FigureColor { id: number; hexCode: string; club: number; selectable: boolean; }
interface FigurePalette { id: number; colors: FigureColor[]; }
interface FigureSet { id: number; gender: 'M' | 'F' | 'U'; club: number; selectable: boolean; }
interface FigureSetType { type: string; paletteId: number; sets: FigureSet[]; }
interface FigureData { palettes: FigurePalette[]; setTypes: FigureSetType[]; }

interface PartSelection { partId: number; colors: number[]; }
type FigureSelection = Record<string, PartSelection>;

const buildFigureString = (selection: FigureSelection): string =>
{
    const seen = new Set<string>();
    const parts: string[] = [];
    const push = (setType: string) =>
    {
        if(seen.has(setType)) return;
        seen.add(setType);
        const sel = selection[setType];
        if(!sel || sel.partId < 0) return;
        const tail = (sel.colors && sel.colors.length) ? `-${ sel.colors.join('-') }` : '';
        parts.push(`${ setType }-${ sel.partId }${ tail }`);
    };
    for(const setType of PART_ROWS) push(setType);
    for(const setType of Object.keys(selection)) push(setType);
    return parts.join('.');
};

const buildImagingUrl = (template: string, figure: string, gender: GenderKey): string =>
    template
        .replace(/\{figure\}/g, encodeURIComponent(figure))
        .replace(/\{gender\}/g, gender)
        .replace(/\{direction\}/g, '2');

const HEAD_ONLY_PARTS = new Set([ 'hr', 'hd' ]);

const buildPartPreviewUrl = (
    template: string,
    setType: string,
    selection: FigureSelection,
    gender: GenderKey
): string =>
{
    const defaults = FALLBACK_DEFAULTS[gender];
    const partSel = selection[setType] ?? defaults[setType];
    const tail = (partSel.colors && partSel.colors.length) ? `-${ partSel.colors.join('-') }` : '';
    const isHeadOnly = HEAD_ONLY_PARTS.has(setType);

    let parts: string[];
    if(isHeadOnly)
    {
        const hd = defaults.hd;
        const pieces = new Map<string, string>();
        pieces.set('hd', `hd-${ hd.partId }-${ hd.colors.join('-') }`);
        pieces.set(setType, `${ setType }-${ partSel.partId }${ tail }`);
        parts = Array.from(pieces.values());
    }
    else
    {
        const hd = defaults.hd;
        parts = [
            `hd-${ hd.partId }-${ hd.colors.join('-') }`,
            `${ setType }-${ partSel.partId }${ tail }`
        ];
    }

    const figure = parts.join('.');
    let url = template
        .replace(/\{figure\}/g, encodeURIComponent(figure))
        .replace(/\{gender\}/g, gender)
        .replace(/\{direction\}/g, '2');

    url = url.replace(/size=l/, 'size=s').replace(/size=m/, 'size=s');
    if(!/size=/.test(url)) url += (url.includes('?') ? '&' : '?') + 'size=s';
    if(isHeadOnly && !/headonly=/.test(url)) url += '&headonly=1';

    return url;
};

const RegisterDialog: FC<RegisterDialogProps> = props =>
{
    const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;

    const [ step, setStep ] = useState<RegisterStep>('credentials');
    const [ email, setEmail ] = useState('');
    const [ password, setPassword ] = useState('');
    const [ confirm, setConfirm ] = useState('');
    const [ username, setUsername ] = useState('');
    const [ gender, setGender ] = useState<GenderKey>('F');
    const [ selection, setSelection ] = useState<FigureSelection>(() => ({ ...FALLBACK_DEFAULTS.F }));
    const [ localError, setLocalError ] = useState<string | null>(null);
    const [ checking, setChecking ] = useState(false);
    const [ turnstileToken, setTurnstileToken ] = useState('');
    const [ resetSignal, setResetSignal ] = useState(0);
    const [ serverReachable, setServerReachable ] = useState<boolean | null>(null);
    const [ pingingServer, setPingingServer ] = useState(false);

    const pingServer = useCallback(async () =>
    {
        setPingingServer(true);
        try
        {
            const ok = await onCheckServer();
            setServerReachable(ok);
            return ok;
        }
        finally
        {
            setPingingServer(false);
        }
    }, [ onCheckServer ]);

    useEffect(() =>
    {
        let cancelled = false;
        (async () =>
        {
            const ok = await onCheckServer();
            if(!cancelled) setServerReachable(ok);
        })();
        return () => { cancelled = true; };
    }, [ onCheckServer ]);

    const resetWidget = useCallback(() =>
    {
        setTurnstileToken('');
        setResetSignal(prev => prev + 1);
    }, []);

    useEffect(() => { setLocalError(null); }, [ step ]);

    const [ figureData, setFigureData ] = useState<FigureData | null>(null);
    const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
    const figureDataUrl = useMemo(() =>
    {
        if(!figureDataUrlRaw) return '';
        try { return GetConfiguration().interpolate(figureDataUrlRaw); }
        catch { return figureDataUrlRaw; }
    }, [ figureDataUrlRaw ]);

    useEffect(() =>
    {
        if(step !== 'avatar' || figureData || !figureDataUrl) return;
        let cancelled = false;
        fetch(figureDataUrl, { credentials: 'omit' })
            .then(r => r.ok ? r.json() : null)
            .then(json => { if(!cancelled && json) setFigureData(json as FigureData); })
            .catch(() => { });
        return () => { cancelled = true; };
    }, [ step, figureData, figureDataUrl ]);

    const partOptions = useMemo(() =>
    {
        const result: Record<string, Record<GenderKey, number[]>> = {};
        if(!figureData) return result;
        for(const st of figureData.setTypes)
        {
            if(!PART_ROWS.includes(st.type)) continue;
            const forGender = (g: GenderKey) => st.sets
                .filter(s => s.selectable && s.club === 0 && (s.gender === g || s.gender === 'U'))
                .map(s => s.id);
            result[st.type] = { M: forGender('M'), F: forGender('F') };
        }
        return result;
    }, [ figureData ]);

    const paletteOptions = useMemo(() =>
    {
        const result: Record<string, { id: number; hex: string }[]> = {};
        if(!figureData) return result;
        for(const st of figureData.setTypes)
        {
            if(!PART_ROWS.includes(st.type)) continue;
            const palette = figureData.palettes.find(p => p.id === st.paletteId);
            if(!palette) { result[st.type] = []; continue; }
            result[st.type] = palette.colors
                .filter(c => c.selectable && c.club === 0)
                .map(c => ({ id: c.id, hex: '#' + c.hexCode.toUpperCase() }));
        }
        return result;
    }, [ figureData ]);

    const hexFor = useCallback((setType: string, colorId: number): string =>
    {
        const list = paletteOptions[setType];
        if(list)
        {
            const found = list.find(c => c.id === colorId);
            if(found) return found.hex;
        }
        return FALLBACK_HEX[colorId] || '#c9c9c9';
    }, [ paletteOptions ]);

    const [ hotLooks, setHotLooks ] = useState<{ gender: GenderKey; figure: string }[]>([]);
    const [ hotLookIndex, setHotLookIndex ] = useState(-1);

    useEffect(() =>
    {
        if(step !== 'avatar' || hotLooks.length) return;
        let cancelled = false;
        fetch(configFileUrl('hotlooks.json', true), { credentials: 'omit' })
            .then(r => r.ok ? r.json() : null)
            .then((json: unknown) =>
            {
                if(cancelled || !Array.isArray(json)) return;
                const parsed: { gender: GenderKey; figure: string }[] = [];
                for(const entry of json as Record<string, unknown>[])
                {
                    const rawGender = typeof entry._gender === 'string' ? entry._gender.toUpperCase() : '';
                    const figure = typeof entry._figure === 'string' ? entry._figure : '';
                    if((rawGender !== 'M' && rawGender !== 'F') || !figure) continue;
                    parsed.push({ gender: rawGender as GenderKey, figure });
                }
                if(parsed.length) setHotLooks(parsed);
            })
            .catch(() => { });
        return () => { cancelled = true; };
    }, [ step, hotLooks.length ]);

    const applyLook = useCallback((figure: string, lookGender: GenderKey) =>
    {
        const next: FigureSelection = {};
        for(const setPart of figure.split('.'))
        {
            const bits = setPart.split('-');
            if(bits.length < 2) continue;
            const setType = bits[0];
            const partId = parseInt(bits[1], 10);
            if(!setType || Number.isNaN(partId)) continue;
            const colors: number[] = [];
            for(let i = 2; i < bits.length; i++)
            {
                const c = parseInt(bits[i], 10);
                if(!Number.isNaN(c)) colors.push(c);
            }
            next[setType] = { partId, colors };
        }

        for(const setType of PART_ROWS)
        {
            if(!next[setType]) next[setType] = { ...FALLBACK_DEFAULTS[lookGender][setType] };
        }
        setGender(lookGender);
        setSelection(next);
    }, []);

    const cycleHotLook = useCallback(() =>
    {
        if(!hotLooks.length) return;
        const nextIdx = (hotLookIndex + 1) % hotLooks.length;
        setHotLookIndex(nextIdx);
        const look = hotLooks[nextIdx];
        applyLook(look.figure, look.gender);
    }, [ hotLooks, hotLookIndex, applyLook ]);

    const credentialsValid =
        EMAIL_REGEX.test(email.trim()) &&
        password.length >= 8 &&
        password === confirm;

    const handleCredentialsNext = async (event: FormEvent<HTMLFormElement>) =>
    {
        event.preventDefault();
        setLocalError(null);

        if(!email.trim() || !password || !confirm)
        {
            setLocalError('Please fill in every field.');
            return;
        }
        if(!EMAIL_REGEX.test(email.trim()))
        {
            setLocalError('Please enter a valid email address.');
            return;
        }
        if(password.length < 8)
        {
            setLocalError('Your password must be at least 8 characters.');
            return;
        }
        if(password !== confirm)
        {
            setLocalError('Passwords do not match.');
            return;
        }

        setChecking(true);
        try
        {
            const serverOk = await pingServer();
            if(!serverOk)
            {
                setLocalError('The gameserver is not running. Please try again later.');
                return;
            }
            const result = await onCheckEmail(email.trim());
            if(!result.available)
            {
                setLocalError(result.error || 'This email is already in use.');
                return;
            }
            setStep('avatar');
        }
        finally
        {
            setChecking(false);
        }
    };

    const applyGender = (newGender: GenderKey) =>
    {
        setGender(newGender);
        setSelection({ ...FALLBACK_DEFAULTS[newGender] });
        setHotLookIndex(-1);
    };

    const getPartList = useCallback((setType: string): number[] =>
    {
        const loaded = partOptions[setType]?.[gender];
        if(loaded && loaded.length) return loaded;
        const fallback = FALLBACK_DEFAULTS[gender][setType]?.partId;
        return fallback !== undefined ? [ fallback ] : [];
    }, [ partOptions, gender ]);

    const getColorList = useCallback((setType: string): number[] =>
    {
        const loaded = paletteOptions[setType];
        if(loaded && loaded.length) return loaded.map(c => c.id);
        const fallback = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0];
        return fallback !== undefined ? [ fallback ] : [];
    }, [ paletteOptions, gender ]);

    const cyclePart = (setType: string, direction: 1 | -1) =>
    {
        const options = getPartList(setType);
        if(!options.length) return;
        const current = selection[setType]?.partId ?? options[0];
        const idx = options.indexOf(current);
        const nextIdx = ((idx === -1 ? 0 : idx) + direction + options.length) % options.length;
        const colors = getColorList(setType);
        setSelection(prev => ({
            ...prev,
            [setType]: {
                partId: options[nextIdx],
                colors: prev[setType]?.colors ?? [ colors[0] ?? 0 ]
            }
        }));
    };

    const cycleColor = (setType: string, direction: 1 | -1) =>
    {
        const colors = getColorList(setType);
        if(!colors.length) return;
        const currentColor = selection[setType]?.colors?.[0] ?? colors[0];
        const idx = colors.indexOf(currentColor);
        const nextIdx = ((idx === -1 ? 0 : idx) + direction + colors.length) % colors.length;
        const parts = getPartList(setType);
        setSelection(prev => ({
            ...prev,
            [setType]: {
                partId: prev[setType]?.partId ?? parts[0],
                colors: [ colors[nextIdx] ]
            }
        }));
    };

    const figure = buildFigureString(selection);
    const previewSrc = buildImagingUrl(imagingUrl, figure, gender);

    const handleAvatarSubmit = async (event: FormEvent<HTMLFormElement>) =>
    {
        event.preventDefault();
        setLocalError(null);

        const trimmed = username.trim();
        if(!trimmed)
        {
            setLocalError('Please choose a Habbo name.');
            return;
        }
        if(trimmed.length < 3 || trimmed.length > 16)
        {
            setLocalError('Habbo name must be 3–16 characters.');
            return;
        }

        if(turnstileEnabled && !turnstileToken)
        {
            setLocalError('Please complete the security check.');
            return;
        }

        setChecking(true);
        try
        {
            const serverOk = await pingServer();
            if(!serverOk)
            {
                setLocalError('The gameserver is not running. Please try again later.');
                return;
            }
            const result = await onCheckUsername(trimmed);
            if(!result.available)
            {
                setLocalError(result.error || 'This Habbo name is already taken.');
                return;
            }
        }
        finally
        {
            setChecking(false);
        }

        onSubmit({
            username: trimmed,
            email: email.trim(),
            password,
            figure,
            gender,
            turnstileToken
        }, resetWidget);
    };

    const busy = submitting || checking || pingingServer;
    const serverOffline = serverReachable === false;

    return (
        <div className="nitro-login-modal">
            <div className={ `dialog ${ step === 'avatar' ? 'dialog-avatar' : '' }` }>
                <div className="nitro-login-card">
                    <div className="card-title">
                        <span>Habbo Details</span>
                        <span className="nitro-card-close-button" role="button" aria-label="Close" onClick={ onCancel } />
                    </div>

                    { step === 'credentials' &&
                        <form className="card-body" onSubmit={ handleCredentialsNext } autoComplete="on">
                            <div className="register-intro">
                                Let's create your account. Enter your email and pick a password — we'll check that email isn't already in use.
                            </div>
                            { serverOffline &&
                                <div className="error-line server-offline">
                                    The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment.
                                    <button type="button" className="retry-link" onClick={ pingServer } disabled={ pingingServer }>
                                        { pingingServer ? 'Checking…' : 'Retry' }
                                    </button>
                                </div>
                            }
                            <div className="field">
                                <label htmlFor="register-email">Email</label>
                                <input id="register-email" type="email" maxLength={ 120 } autoComplete="email"
                                    value={ email } onChange={ e => setEmail(e.target.value) } />
                            </div>
                            <div className="field">
                                <label htmlFor="register-password">Password</label>
                                <input id="register-password" type="password" maxLength={ 128 } autoComplete="new-password"
                                    value={ password } onChange={ e => setPassword(e.target.value) } />
                            </div>
                            <div className="field">
                                <label htmlFor="register-confirm">Confirm password</label>
                                <input id="register-confirm" type="password" maxLength={ 128 } autoComplete="new-password"
                                    value={ confirm } onChange={ e => setConfirm(e.target.value) } />
                            </div>
                            { (localError || error) && <div className="error-line">{ localError || error }</div> }
                            { info && <div className="info-line">{ info }</div> }
                            <div className="step-footer">
                                <span className="step-indicator">1/2</span>
                                <button type="submit" className="ok-button" disabled={ !credentialsValid || busy || serverOffline }>
                                    { checking || pingingServer ? 'Checking…' : 'Next' }
                                </button>
                            </div>
                        </form>
                    }

                    { step === 'avatar' &&
                        <form className="card-body" onSubmit={ handleAvatarSubmit } autoComplete="on">
                            <div className="register-intro">
                                Now it's time to make your own Habbo character! To make your own Habbo, please start by choosing your Habbo Name.
                            </div>
                            { serverOffline &&
                                <div className="error-line server-offline">
                                    The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment.
                                    <button type="button" className="retry-link" onClick={ pingServer } disabled={ pingingServer }>
                                        { pingingServer ? 'Checking…' : 'Retry' }
                                    </button>
                                </div>
                            }
                            <div className="field">
                                <input id="register-username" type="text" maxLength={ 16 } autoComplete="username" placeholder="HabboName"
                                    value={ username } onChange={ e => setUsername(e.target.value) } />
                            </div>

                            <div className="gender-row">
                                <label>
                                    <input type="radio" name="register-gender" checked={ gender === 'F' } onChange={ () => applyGender('F') } />
                                    <span>Girl</span>
                                </label>
                                <label>
                                    <input type="radio" name="register-gender" checked={ gender === 'M' } onChange={ () => applyGender('M') } />
                                    <span>Boy</span>
                                </label>
                            </div>

                            <div className="avatar-builder">
                                <div className="avatar-part-col">
                                    { PART_ROWS.map(setType => {
                                        const partPreviewSrc = buildPartPreviewUrl(imagingUrl, setType, selection, gender);
                                        return (
                                            <div className="avatar-part-row" key={ `part-${ setType }` }>
                                                <button type="button" className="arrow-btn" aria-label={ `Previous ${ setType }` }
                                                    onClick={ () => cyclePart(setType, -1) }>&lsaquo;</button>
                                                <div className={ `part-preview part-preview-${ setType }` }>
                                                    <img src={ partPreviewSrc } alt={ `${ setType } preview` } onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
                                                </div>
                                                <button type="button" className="arrow-btn" aria-label={ `Next ${ setType }` }
                                                    onClick={ () => cyclePart(setType, 1) }>&rsaquo;</button>
                                            </div>
                                        );
                                    }) }
                                </div>

                                <div className="avatar-preview">
                                    <img src={ previewSrc } alt="Habbo preview" onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } />
                                </div>

                                <div className="avatar-color-col">
                                    { PART_ROWS.map(setType => {
                                        const fallbackColor = FALLBACK_DEFAULTS[gender][setType]?.colors?.[0] ?? 0;
                                        const currentColor = selection[setType]?.colors?.[0] ?? fallbackColor;
                                        const swatchHex = hexFor(setType, currentColor);
                                        return (
                                            <div className="avatar-color-row" key={ `color-${ setType }` }>
                                                <button type="button" className="arrow-btn" aria-label={ `Previous color ${ setType }` }
                                                    onClick={ () => cycleColor(setType, -1) }>&lsaquo;</button>
                                                <div className="color-swatch" style={ { background: swatchHex } } />
                                                <button type="button" className="arrow-btn" aria-label={ `Next color ${ setType }` }
                                                    onClick={ () => cycleColor(setType, 1) }>&rsaquo;</button>
                                            </div>
                                        );
                                    }) }
                                </div>
                            </div>

                            <div className="hot-looks-row">
                                <button type="button" className="ok-button hot-looks-button"
                                    onClick={ cycleHotLook }
                                    disabled={ !hotLooks.length || busy }
                                    title={ hotLooks.length ? `${ hotLooks.length } looks available` : 'No hot looks loaded' }>
                                    Hot Looks{ hotLookIndex >= 0 && hotLooks.length ? ` (${ hotLookIndex + 1 }/${ hotLooks.length })` : '' }
                                </button>
                            </div>

                            { turnstileEnabled &&
                                <TurnstileWidget
                                    siteKey={ turnstileSiteKey }
                                    size="compact"
                                    onToken={ setTurnstileToken }
                                    onExpire={ () => setTurnstileToken('') }
                                    onError={ () => setTurnstileToken('') }
                                    resetSignal={ resetSignal }
                                /> }
                            { (localError || error) && <div className="error-line">{ localError || error }</div> }
                            { info && <div className="info-line">{ info }</div> }

                            <div className="step-footer step-footer-split">
                                <button type="button" className="ok-button back-button" onClick={ () => setStep('credentials') } disabled={ busy }>Back</button>
                                <span className="step-indicator">2/2</span>
                                <button type="submit" className="ok-button" disabled={ !username.trim() || busy || serverOffline }>
                                    { submitting ? 'Creating…' : (checking || pingingServer) ? 'Checking…' : 'Next' }
                                </button>
                            </div>
                        </form>
                    }
                </div>
            </div>
        </div>
    );
};


interface ForgotDialogProps extends DialogSharedProps
{
    onSubmit: (body: { email: string; turnstileToken: string; }, onDialogReset: () => void) => void;
}

const ForgotDialog: FC<ForgotDialogProps> = props =>
{
    const { onCancel, onSubmit, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;
    const [ email, setEmail ] = useState('');
    const [ localError, setLocalError ] = useState<string | null>(null);
    const [ turnstileToken, setTurnstileToken ] = useState('');
    const [ resetSignal, setResetSignal ] = useState(0);

    const resetWidget = useCallback(() =>
    {
        setTurnstileToken('');
        setResetSignal(prev => prev + 1);
    }, []);

    const handle = (event: FormEvent<HTMLFormElement>) =>
    {
        event.preventDefault();
        setLocalError(null);

        if(!email.trim())
        {
            setLocalError('Please enter your email address.');
            return;
        }

        onSubmit({ email: email.trim(), turnstileToken }, resetWidget);
    };

    return (
        <div className="nitro-login-modal">
            <div className="dialog">
                <div className="nitro-login-card">
                    <div className="card-title">
                        <span>Reset password</span>
                        <span className="nitro-card-close-button" role="button" aria-label="Close" onClick={ onCancel } />
                    </div>
                    <form className="card-body" onSubmit={ handle } autoComplete="on">
                        <div className="field">
                            <label htmlFor="forgot-email">Email address</label>
                            <input id="forgot-email" type="email" maxLength={ 120 } autoComplete="email"
                                value={ email } onChange={ e => setEmail(e.target.value) } />
                        </div>
                        { turnstileEnabled &&
                            <TurnstileWidget
                                siteKey={ turnstileSiteKey }
                                size="compact"
                                onToken={ setTurnstileToken }
                                onExpire={ () => setTurnstileToken('') }
                                onError={ () => setTurnstileToken('') }
                                resetSignal={ resetSignal }
                            /> }
                        { (localError || error) && <div className="error-line">{ localError || error }</div> }
                        { info && <div className="info-line">{ info }</div> }
                        <div className="submit-row">
                            <button type="submit" className="ok-button" disabled={ submitting }>Send email</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    );
};
