/**
 * SyncSheet — the Cloud backup surface (slice 6).
 *
 * A bottom sheet, the same idiom as `ReportBrandingSheet`: last backup line,
 * "Back up now" / "Restore", a progress line while a run is in flight, a plain
 * result summary and any errors listed verbatim — a sync that half-worked says
 * so rather than claiming a green tick.
 *
 * There is no "Reload" button any more (review F1): a restore lands in the live
 * stores through their importers, so the records are on screen the moment the
 * sheet says they are.
 *
 * Gating (brief §4, §6 slice 6): Professional+ via `useCloudSync()`. Below that
 * the sheet still opens and explains itself — the buttons are disabled with
 * `SY_LOCKED_COPY`. Signed out it says "Sign in to back up" and makes no
 * request at all. `CLOUD_SYNC` off, or an empty `syncEndpoint`, and the row in
 * the header menu never renders in the first place.
 *
 * Rendered through a PORTAL to document.body: its trigger lives inside the
 * sticky header, which carries `backdrop-blur-md`, and a backdrop-filter
 * ancestor becomes the containing block for `position: fixed` descendants —
 * the sheet would be clipped into the header without this.
 */

var _useState_SY    = React.useState;
var _useCallback_SY = React.useCallback;
var _useEffect_SY   = React.useEffect;
var _useRef_SY      = React.useRef;

/** syFormatWhen — "Never" / "Today 14:02" / "19 Sep 2026" for the last-run line. */
function syFormatWhen(iso) {
    if (!iso) return 'Never';
    var parsed = Date.parse(iso);
    if (isNaN(parsed)) return 'Never';
    var date = new Date(parsed);
    var today = new Date();
    var sameDay = date.getFullYear() === today.getFullYear() &&
                  date.getMonth() === today.getMonth() &&
                  date.getDate() === today.getDate();
    var time = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    if (sameDay) return 'Today ' + time;
    return date.toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' }) + ' ' + time;
}

/**
 * SyncCloudIcon — inline SVG, not a ligature.
 *
 * `cloud_upload` and `backup` are both OUTSIDE the Material Symbols subset
 * (`assets/fonts/symbols.icons.txt`), and an unlisted name renders as literal
 * ligature text (CLAUDE.md hard rule). `cloud` IS in the list, but it is the
 * weather glyph and says nothing about backing up, so the row draws its own.
 */
var SyncCloudIcon = function (props) {
    var cls = props && props.className ? props.className : '';
    return (
        <svg className={cls} viewBox="0 0 24 24" width="20" height="20" fill="none"
             stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
             aria-hidden="true" focusable="false">
            <path d="M18 16.5a3.5 3.5 0 0 0-.6-6.95 5.5 5.5 0 0 0-10.7-1.2A4 4 0 0 0 6.5 16.5" />
            <path d="M12 21v-8" />
            <path d="m9 15.5 3-3 3 3" />
        </svg>
    );
};

/** syScanCount — "1 scan" / "3 scans". */
function syScanCount(n) { return n + ' scan' + (n === 1 ? '' : 's'); }

/** syShortDate — "12 Oct 2026" for the scan list in the ownership question. */
function syShortDate(ms) {
    if (typeof ms !== 'number' || !isFinite(ms)) return '';
    return new Date(ms).toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' });
}

/**
 * SyncLinkQuestion — P-08 "ask on first backup" (Rozario, P-07, 2026-10-13).
 *
 * Shown INSIDE the sheet, in place of the Back up / Restore row, when the
 * person presses Back up now and this device holds scans no account has
 * claimed (saved signed out, before ownership existed, or by someone else on
 * a shared device). Nothing is uploaded until they answer. A calm cream panel,
 * not a warning: nothing is wrong, we just can't know whose scans these are.
 *
 *   mode 'unlinked'   — first-backup question: Back them up / Keep on this
 *                       device only / Skip these for now (backs up the rest).
 *   mode 'deviceOnly' — reopened from the "kept on this device" line: Back
 *                       them up / Keep them on this device.
 *
 * Focus goes to the heading (DESIGN.md §6), not to an action, so a stray
 * Enter never uploads anything. Every button is ≥ 44 px.
 */
var SyncLinkQuestion = function (props) {
    var items = props.items || [];
    var mode = props.mode;
    var headingRef = _useRef_SY(null);
    _useEffect_SY(function () {
        if (headingRef.current) headingRef.current.focus();
    }, []);
    var n = items.length;
    var who = props.email || 'this account';
    var shown = items.slice(0, 3);
    var btn = 'w-full min-h-[44px] rounded-full px-4 py-3 text-sm font-extrabold transition-all duration-200 ';
    return (
        <div data-testid="sync-link-question" role="group" aria-labelledby="sync-link-title" aria-describedby="sync-link-body"
             className="rounded-2xl bg-background-light px-4 py-4 space-y-3">
            <h3 id="sync-link-title" ref={headingRef} tabIndex={-1} className="text-sm font-extrabold text-forest outline-none">
                {mode === 'deviceOnly'
                    ? syScanCount(n) + (n === 1 ? ' is' : ' are') + ' kept on this device only'
                    : syScanCount(n) + ' on this device ' + (n === 1 ? 'isn’t' : 'aren’t') + ' linked to ' + who + ' yet'}
            </h3>
            <p id="sync-link-body" className="text-[12px] font-semibold text-forest/80">
                {mode === 'deviceOnly'
                    ? 'They aren’t in anyone’s cloud backup. Back them up to ' + who + ' only if they’re yours.'
                    : 'They were saved while signed out, or by someone else who uses this device. Back them up only if they’re yours. Once linked, they show only when you’re signed in.'}
            </p>
            <ul data-testid="sync-link-list" className="space-y-1">
                {shown.map(function (item) {
                    return (
                        <li key={item.id} className="flex justify-between gap-3 text-[12px] font-bold text-forest">
                            <span className="truncate">{item.label}</span>
                            <span className="shrink-0 font-semibold text-forest/70">{syShortDate(item.timestamp)}</span>
                        </li>
                    );
                })}
                {n > shown.length && (
                    <li className="text-[12px] font-semibold text-forest/70">and {n - shown.length} more</li>
                )}
            </ul>
            <div className="space-y-2 pt-1">
                <button type="button" data-testid="sync-link-yes" disabled={props.busy}
                        onClick={function () { props.onAnswer('link'); }}
                        className={btn + 'bg-primary text-forest btn-nature'}>
                    Back them up to this account
                </button>
                <button type="button" data-testid="sync-link-device" disabled={props.busy}
                        onClick={function () { props.onAnswer('device'); }}
                        className={btn + 'bg-surface text-forest border border-sage btn-nature'}>
                    {mode === 'deviceOnly' ? 'Keep them on this device' : 'Keep on this device only'}
                </button>
                {mode !== 'deviceOnly' && (
                    <button type="button" data-testid="sync-link-later" disabled={props.busy}
                            onClick={function () { props.onAnswer('later'); }}
                            className={btn + 'text-forest underline underline-offset-2'}>
                        Skip these for now
                    </button>
                )}
            </div>
            <p className="text-[11px] font-semibold text-forest/70">
                {mode === 'deviceOnly'
                    ? 'Scans kept on this device are never uploaded.'
                    : 'Kept scans stay on this device and are never uploaded. If you skip, the rest of your data is backed up and we’ll ask again next time.'}
            </p>
        </div>
    );
};

var SyncSheet = function (props) {
    var onClose = props.onClose;
    var gate    = useCloudSync();
    var auth    = useClerkAuth();

    var stateState = _useState_SY(null);
    var syncState  = stateState[0];
    var setSyncState = stateState[1];

    var busyState = _useState_SY('');       // '' | 'backup' | 'restore'
    var busy      = busyState[0];
    var setBusy   = busyState[1];

    var progressState = _useState_SY('');
    var progress      = progressState[0];
    var setProgress   = progressState[1];

    var resultState = _useState_SY(null);
    var result      = resultState[0];
    var setResult   = resultState[1];

    // P-08: the scans on this device no account has claimed, and the
    // open question about them (null | { mode, items }).
    var ownershipState = _useState_SY(null);
    var ownership      = ownershipState[0];
    var setOwnership   = ownershipState[1];

    var askState = _useState_SY(null);
    var ask      = askState[0];
    var setAsk   = askState[1];

    var aliveRef = _useRef_SY(true);
    _useEffect_SY(function () {
        aliveRef.current = true;
        return function () { aliveRef.current = false; };
    }, []);

    var signedIn = !!auth.isSignedIn;

    // Q-01 F1: when signed in, bind the receipt to THIS account on open, so a
    // reset after an account switch (and its note) shows before any button.
    _useEffect_SY(function () {
        var load = (signedIn && SyncService.bindAccount)
            ? SyncService.bindAccount({ getToken: auth.getToken })
            : SyncService.getState();
        load.then(function (state) {
            if (aliveRef.current) setSyncState(state);
        });
        if (signedIn && SyncService.scanOwnership) {
            SyncService.scanOwnership({ getToken: auth.getToken }).then(function (o) {
                if (aliveRef.current) setOwnership(o);
            });
        } else {
            setOwnership(null);
        }
    }, [signedIn]);
    var canRun   = gate.entitled && signedIn && !busy && !ask;

    var run = _useCallback_SY(function (which) {
        if (!gate.entitled || !signedIn) return;
        setBusy(which);
        setResult(null);
        setProgress('');
        var options = {
            getToken: auth.getToken,
            onProgress: function (text) { if (aliveRef.current) setProgress(text); }
        };
        var call = (which === 'backup') ? SyncService.backup(options) : SyncService.restore({
            merge: true, getToken: options.getToken, onProgress: options.onProgress
        });
        call.then(function (res) {
            if (!aliveRef.current) return;
            setResult(res);
            setBusy('');
            setProgress('');
            SyncService.getState().then(function (state) {
                if (aliveRef.current) setSyncState(state);
            });
            if (SyncService.scanOwnership) {
                SyncService.scanOwnership({ getToken: auth.getToken }).then(function (o) {
                    if (aliveRef.current) setOwnership(o);
                });
            }
        });
    }, [gate.entitled, signedIn, auth.getToken]);

    /**
     * startBackup — P-08: before uploading anything, look (fresh, from the
     * same rows a backup reads) for scans no account has claimed. Any there
     * → ask; none → back up straight away, exactly as before.
     */
    var startBackup = _useCallback_SY(function () {
        if (!gate.entitled || !signedIn) return;
        if (!SyncService.scanOwnership) { run('backup'); return; }
        setBusy('checking');
        setResult(null);
        SyncService.scanOwnership({ getToken: auth.getToken }).then(function (o) {
            if (!aliveRef.current) return;
            setOwnership(o);
            setBusy('');
            if (o && o.unlinked && o.unlinked.length > 0) setAsk({ mode: 'unlinked', items: o.unlinked });
            else run('backup');
        });
    }, [gate.entitled, signedIn, auth.getToken, run]);

    /** answer — the person's choice in SyncLinkQuestion. */
    var answer = _useCallback_SY(function (choice) {
        if (!ask) return;
        var ids = ask.items.map(function (it) { return it.id; });
        var mode = ask.mode;
        if (choice === 'later' || (mode === 'deviceOnly' && choice === 'device')) {
            setAsk(null);
            if (mode === 'unlinked') run('backup');      // the rest is backed up; these wait
            return;
        }
        setBusy('linking');
        SyncService.linkScans(ids, choice === 'link' ? 'link' : 'device', { getToken: auth.getToken }).then(function () {
            if (!aliveRef.current) return;
            setBusy('');
            setAsk(null);
            if (choice === 'link' || mode === 'unlinked') run('backup');
            else SyncService.scanOwnership({ getToken: auth.getToken }).then(function (o) { if (aliveRef.current) setOwnership(o); });
        });
    }, [ask, run, auth.getToken]);

    var lastBackup  = syncState ? syFormatWhen(syncState.lastBackupAt) : '…';
    var lastRestore = syncState ? syFormatWhen(syncState.lastRestoreAt) : '…';
    var counts      = (syncState && syncState.counts) || { scans: 0, properties: 0, assessments: 0, profile: 0, branding: 0, 'component-photos': 0 };

    var summary = null;
    if (result) {
        if (result.restored > 0) summary = 'Restored ' + result.restored + ' record' + (result.restored === 1 ? '' : 's') + '.';
        else if (result.uploaded > 0) summary = 'Backed up ' + result.uploaded + ' file' + (result.uploaded === 1 ? '' : 's') + '.';
        else summary = result.reason || ((result.removed > 0 || result.merged > 0) ? 'Up to date.' : 'Nothing changed.');
        if (result.reason && (result.uploaded > 0 || result.restored > 0)) summary += ' ' + result.reason;
        // Say what was skipped and why, in the user's terms. An unchanged
        // object is a saving; a deferred one is a promise to try again — and
        // both are the difference between "nothing happened" and "nothing
        // needed to happen", which a progress bar alone cannot tell you.
        if (result.unchanged > 0) summary += ' ' + result.unchanged + ' already up to date.';
        if (result.deferred > 0) summary += ' ' + result.deferred + ' will be retried shortly.';
        if (result.deleted > 0) summary += ' Removed ' + result.deleted + ' deleted item' + (result.deleted === 1 ? '' : 's') + ' from the backup.';
        // Contract 1.0 (CLIENT-CONTRACT.md R10): say when another device's work
        // was combined in, and when a delete made elsewhere reached this device —
        // both change what is on screen, so neither may happen silently.
        if (result.merged > 0) summary += ' Combined ' + result.merged + ' record' + (result.merged === 1 ? '' : 's') + ' with changes from your other device.';
        if (result.removed > 0) summary += ' Removed ' + result.removed + ' item' + (result.removed === 1 ? '' : 's') + ' you deleted on another device.';
        // P-08: say what was left out and that we will ask again.
        if (result.unlinked > 0) summary += ' ' + syScanCount(result.unlinked) + ' on this device ' + (result.unlinked === 1 ? 'wasn’t' : 'weren’t') + ' backed up because ' + (result.unlinked === 1 ? 'it isn’t' : 'they aren’t') + ' linked to this account yet — you’ll be asked next time.';
    }
    var deviceOnly = (ownership && ownership.deviceOnly) || [];
    var userEmail = (auth.user && auth.user.email) || '';

    var sheet = (
        <div
            className="fixed inset-0 z-[110] flex items-end justify-center bg-forest/40 backdrop-blur-sm"
            data-testid="sync-sheet"
            onClick={function (e) { if (e.target === e.currentTarget) onClose(); }}
        >
            <div className="w-full max-w-md bg-surface rounded-t-3xl shadow-soft max-h-[88vh] overflow-y-auto">
                <div className="flex items-center gap-3 px-5 pt-5 pb-3 border-b border-stone-100/60">
                    <span className="flex items-center justify-center size-9 rounded-full bg-background-light text-forest shrink-0">
                        <SyncCloudIcon className="text-forest" />
                    </span>
                    <div className="flex-1 min-w-0">
                        <h2 className="text-base font-extrabold text-forest">Cloud backup</h2>
                        <p className="text-[11px] font-semibold text-muted">Your scans, assessments, profile and report branding</p>
                    </div>
                    <button
                        onClick={onClose}
                        data-testid="sync-sheet-close"
                        className="flex items-center justify-center size-9 rounded-full bg-background-light text-forest"
                        aria-label="Close"
                    >
                        <span className="material-symbols-outlined text-[20px]">close</span>
                    </button>
                </div>

                <div className="px-5 py-4 space-y-3">
                    <div className="rounded-2xl bg-background-light px-4 py-3">
                        <p className="text-[11px] font-bold text-muted uppercase tracking-wide">Last backup</p>
                        <p data-testid="sync-last-backup" className="text-sm font-extrabold text-forest">{lastBackup}</p>
                        {syncState && syncState.lastBackupAt && (
                            <p data-testid="sync-last-counts" className="text-[11px] font-semibold text-muted mt-0.5">
                                {counts.scans} scan{counts.scans === 1 ? '' : 's'} ·{' '}
                                {counts.properties} propert{counts.properties === 1 ? 'y' : 'ies'} ·{' '}
                                {counts.assessments} assessment{counts.assessments === 1 ? '' : 's'} ·{' '}
                                {counts.profile ? 'profile' : 'no profile'} ·{' '}
                                {counts.branding ? 'branding' : 'no branding'}
                                {counts['component-photos'] > 0 && (
                                    ' · ' + counts['component-photos'] + ' component photo' + (counts['component-photos'] === 1 ? '' : 's')
                                )}
                            </p>
                        )}
                        <p data-testid="sync-last-restore" className="text-[11px] font-semibold text-muted mt-1">
                            Last restore: {lastRestore}
                        </p>
                    </div>

                    {syncState && syncState.accountReset && (
                        <p data-testid="sync-account-reset" role="status" className="text-[11px] font-bold text-forest bg-warning-light rounded-xl px-3 py-2">
                            {syncState.accountReset.reason === 'switched'
                                ? 'You’re signed in with a different account from this device’s last backup. Its backup history here has been cleared, so the next backup uploads everything to this account.'
                                : 'This device’s backup history couldn’t be matched to your account, so it has been cleared. The next backup uploads everything to this account.'}
                        </p>
                    )}

                    {!gate.entitled && (
                        <p data-testid="sync-locked-copy" className="text-[11px] font-bold text-forest bg-warning-light rounded-xl px-3 py-2">
                            {gate.lockedCopy}
                        </p>
                    )}

                    {gate.entitled && !signedIn && (
                        <p data-testid="sync-signed-out" className="text-[11px] font-bold text-forest bg-warning-light rounded-xl px-3 py-2">
                            Sign in to back up
                        </p>
                    )}

                    {ask && (
                        <SyncLinkQuestion mode={ask.mode} items={ask.items} email={userEmail}
                                          busy={busy === 'linking'} onAnswer={answer} />
                    )}

                    {!ask && <div className="flex gap-2">
                        <button
                            data-testid="sync-backup"
                            disabled={!canRun}
                            onClick={startBackup}
                            className={'flex-1 rounded-full px-4 py-3 text-sm font-extrabold transition-all duration-200 ' +
                                (canRun ? 'bg-primary text-white btn-nature' : 'bg-background-light text-muted cursor-not-allowed')}
                        >
                            {busy === 'backup' ? 'Backing up…' : 'Back up now'}
                        </button>
                        <button
                            data-testid="sync-restore"
                            disabled={!canRun}
                            onClick={function () { run('restore'); }}
                            className={'flex-1 rounded-full px-4 py-3 text-sm font-extrabold transition-all duration-200 ' +
                                (canRun ? 'bg-surface text-forest border border-sage btn-nature' : 'bg-background-light text-muted cursor-not-allowed')}
                        >
                            {busy === 'restore' ? 'Restoring…' : 'Restore'}
                        </button>
                    </div>}

                    {!ask && gate.entitled && signedIn && deviceOnly.length > 0 && (
                        <p data-testid="sync-device-only" className="text-[11px] font-semibold text-forest">
                            {syScanCount(deviceOnly.length)} on this device {deviceOnly.length === 1 ? 'is' : 'are'} kept off cloud backup.{' '}
                            <button type="button" data-testid="sync-device-only-review" disabled={!!busy}
                                    onClick={function () { setResult(null); setAsk({ mode: 'deviceOnly', items: deviceOnly }); }}
                                    className="inline-flex items-center min-h-[44px] font-extrabold underline underline-offset-2">
                                Review
                            </button>
                        </p>
                    )}

                    {progress && (
                        <p data-testid="sync-progress" className="text-[11px] font-bold text-forest">{progress}</p>
                    )}

                    {summary && (
                        <p data-testid="sync-summary" className="text-sm font-bold text-forest">{summary}</p>
                    )}

                    {result && result.errors && result.errors.length > 0 && (
                        <ul data-testid="sync-errors" className="space-y-1">
                            {result.errors.map(function (line, i) {
                                return (
                                    <li key={i} className="text-[11px] font-semibold text-forest bg-warning-light rounded-xl px-3 py-2 break-words">
                                        {line}
                                    </li>
                                );
                            })}
                        </ul>
                    )}

                    <p className="text-[11px] font-medium text-muted">
                        Backups are private to your account. Photos are uploaded straight to storage —
                        Mould Detect's sync service only checks who you are.
                    </p>
                </div>
            </div>
        </div>
    );

    return ReactDOM.createPortal(sheet, document.body);
};

window.SyncSheet     = SyncSheet;
window.SyncLinkQuestion = SyncLinkQuestion;
window.SyncCloudIcon = SyncCloudIcon;
window.syFormatWhen  = syFormatWhen;
