/*
    Client-side live preview bridge for the Master Control tenant form.

    The theme and invoice fields are not `->live()`, so keystrokes do not
    round-trip to the server just to repaint the two previews; the previews are
    repainted in the browser instead. The full per-preset colour sets, font
    stacks, radius/scale steps and currency symbols are embedded by
    `preview-bridge.blade.php` into `window.previewBridgeConfig` (they are
    server-generated), and this script rebuilds the previews from the form's
    CURRENT state whenever a relevant field changes.

    It writes through the SAME helpers the server render uses
    (TenantTheme::previewVariables(), App\Support\Invoice\InvoiceDocument::money()),
    so a keystroke cannot preview something a save would not produce.

    Wiring:
      - `preview:change` on window — dispatched by the preset picker and the
        theme/currency Selects (their wrapper attrs announce state changed;
        picking with mouse or keyboard both bubble to it).
      - `input` events on wire:model elements — the deferred text fields; the
        bridge checks the wire path to decide which preview to repaint.
      - every successful Livewire commit — covers logo uploads, whose server
        re-render would otherwise repaint the previews from stale deferred
        state.
*/
(() => {
    const config = window.previewBridgeConfig;
    if (!config) return;

    const {
        presets,
        fontStacks,
        radii,
        scales,
        currencySymbols,
        defaultPreset,
        defaultFont,
        fallbackAppName,
        savedCurrency,
    } = config;

    const cssText = (vars) => Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(';') + ';';

    const resolvePath = (obj, path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);

    const read = (key) => {
        const el = document.querySelector('[data-preview-bind="' + key + '"]');
        if (!el) return null;
        if (! window.Alpine || ! el._x_dataStack) return null;
        return resolvePath(window.Alpine.$data(el), el.getAttribute('data-preview-value') || 'state');
    };

    // The text fields carry no markers — they are found by their wire:model
    // path instead, so the bridge needs no per-field knowledge beyond the
    // path, and can never drift from what is actually submitted.
    const readWire = (path) => {
        const el = Array.from(document.querySelectorAll('input, textarea')).find((el) =>
            el.getAttributeNames().some((name) => name.startsWith('wire:model') && el.getAttribute(name) === path)
        );
        return el ? el.value : null;
    };

    const settingsField = (key) => readWire('data.settings.' + key) ?? null;

    const wireModelPath = (el) => {
        const wire = el.getAttributeNames().find((name) => name.startsWith('wire:model'));
        return wire ? el.getAttribute(wire) : null;
    };

    // ── Theme preview ────────────────────────────────────────────────

    // The theme Selects are x-load components: they boot a moment after
    // the page, so the first repaint may not be able to read them. The
    // server render is already correct at that point, so skipping is
    // safer than repainting from fallbacks. font_heading is nullable, so
    // it is deliberately not part of the readiness check.
    const themeStateReadable = () => ['theme.preset', 'theme.font_body', 'theme.radius', 'theme.scale']
        .every((key) => read(key) !== null);

    const themeVars = () => {
        const presetKey = read('theme.preset') || defaultPreset;
        const bodyKey = read('theme.font_body') || defaultFont;
        const headingKey = read('theme.font_heading') || bodyKey;
        const radiusKey = read('theme.radius') || 'md';
        const scaleKey = read('theme.scale') || 'default';

        const preset = presets[presetKey] || presets[defaultPreset];
        const radius = radii[radiusKey] ?? radii.md;
        const scale = scales[scaleKey] ?? scales.default;

        // Mirrors TenantTheme::typography() — including the derived radius
        // steps, which is what actually makes the setting reach the shell.
        const typography = {
            '--font-sans-family': fontStacks[bodyKey] || fontStacks[defaultFont],
            '--font-heading-family': fontStacks[headingKey] || fontStacks[defaultFont],
            '--radius': radius,
            '--root-font-size': scale,
            '--radius-sm': 'calc(var(--radius) * 0.5)',
            '--radius-md': 'calc(var(--radius) * 0.75)',
            '--radius-lg': 'var(--radius)',
            '--radius-xl': 'calc(var(--radius) * 1.5)',
            '--radius-2xl': 'calc(var(--radius) * 2)',
        };

        return {
            light: cssText(Object.assign({}, preset.light, typography)),
            dark: cssText(Object.assign({}, preset.dark, typography)),
        };
    };

    const refreshTheme = () => {
        if (! themeStateReadable()) return;

        const vars = themeVars();

        document.querySelectorAll('.fi-tp-vars').forEach((el) => {
            if (el._x_dataStack) {
                window.Alpine.$data(el).styles = { light: vars.light, dark: vars.dark };
            }
        });
    };

    const refreshBrand = () => {
        const name = (settingsField('branding.app_name') || '').trim() || fallbackAppName;

        document.querySelectorAll('[data-tp-brand]').forEach((el) => {
            if (el.textContent !== name) el.textContent = name;
        });

        document.querySelectorAll('[data-tp-avatar]').forEach((el) => {
            const letter = name.charAt(0).toUpperCase();
            if (el.textContent !== letter) el.textContent = letter;
        });
    };

    // ── Invoice preview ──────────────────────────────────────────────

    const invoiceValues = () => ({
        business_name: settingsField('invoice.business_name') ?? '',
        business_description: settingsField('invoice.business_description') ?? '',
        business_address: settingsField('invoice.business_address') ?? '',
        business_phone: settingsField('invoice.business_phone') ?? '',
        business_email: settingsField('invoice.business_email') ?? '',
        business_website: settingsField('invoice.business_website') ?? '',
        business_registration_no: settingsField('invoice.business_registration_no') ?? '',
        tax_registration_no: settingsField('invoice.tax_registration_no') ?? '',
    });

    const refreshInvoice = () => {
        const v = invoiceValues();

        document.querySelectorAll('[data-inv-bind]').forEach((el) => {
            const value = (v[el.dataset.invBind] || '').trim();
            el.textContent = value ? (el.dataset.invPrefix || '') + value : '';
            el.style.display = value ? '' : 'none';
        });

        // The registration pair shares one line on A4; rebuild it whole so
        // the " · " separator only shows when both sides are present.
        document.querySelectorAll('[data-inv-reg]').forEach((el) => {
            const parts = [];
            if ((v.business_registration_no || '').trim()) parts.push('Reg. No: ' + v.business_registration_no.trim());
            if ((v.tax_registration_no || '').trim()) parts.push('VAT/TIN: ' + v.tax_registration_no.trim());
            el.textContent = parts.join(' · ');
            el.style.display = parts.length ? '' : 'none';
        });

        // A VAT-registered seller heads the document "Tax Invoice".
        document.querySelectorAll('[data-inv-title]').forEach((el) => {
            const title = (v.tax_registration_no || '').trim() ? 'Tax Invoice' : 'Invoice';
            el.textContent = el.dataset.invTitle === 'upper' ? title.toUpperCase() : title;
        });

        refreshAmounts();
    };

    const fmtAmount = (n) => {
        const [int, dec] = Math.abs(n).toFixed(2).split('.');
        return int.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '.' + dec;
    };

    const refreshAmounts = () => {
        const symbol = currencySymbols[read('locale.currency') || savedCurrency] || 'Rs.';

        document.querySelectorAll('[data-inv-amount]').forEach((el) => {
            const n = parseFloat(el.dataset.invAmount || '0');
            const sign = n < 0 ? '-' : '';
            const text = sign + (el.dataset.invSymbol ? symbol + ' ' : '') + fmtAmount(n);
            if (el.textContent !== text) el.textContent = text;
        });
    };

    // ── Wiring ───────────────────────────────────────────────────────

    const refresh = () => {
        refreshTheme();
        refreshBrand();
        refreshInvoice();
    };

    document.addEventListener('input', (event) => {
        const path = wireModelPath(event.target);
        if (! path || ! path.startsWith('data.settings.')) return;

        const field = path.slice('data.settings.'.length);

        if (field === 'branding.app_name') {
            refreshBrand();
        } else if (field.startsWith('invoice.')) {
            refreshInvoice();
        }
    });

    window.addEventListener('preview:change', () => refresh());

    // The selects load lazily and the picker keeps its own Alpine state, so
    // the first pass may read nothing; the previews are server-rendered
    // correct anyway, and every change re-runs this.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', refresh);
    } else {
        refresh();
    }

    // A logo upload re-renders the whole form server-side, which repaints
    // both previews from the last committed state — including any deferred
    // edits that came with that same commit, so the repaint is already
    // right; this re-applies the live values on top regardless.
    document.addEventListener('livewire:init', () => {
        Livewire.hook('commit', ({ succeed }) => {
            succeed(() => refresh());
        });
    });
})();