'use client';
/**
 * This Source Code is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) Infonomic Company Limited
 */
/**
 * Scheduled publication in the document editor.
 *
 * The feature is split across three surfaces rather than one block, because
 * the states differ in how much of the editor's attention they deserve:
 *
 *   - The **actions** live in the document-actions menu, next to the other
 *     document-level operations, so the status bar's primary Save / Publish
 *     controls keep their weight.
 *   - An **armed** schedule renders as one more metadata cell in the status
 *     bar, alongside Status and Last modified. It is a fact about the
 *     document, presented at the scale of the other facts.
 *   - **Needs re-confirmation**, **overdue** and **failing** schedules
 *     escalate to a non-dismissible Alert below the status bar, carrying
 *     their own actions. The `needs_reconfirm` notice in particular has to
 *     outlive the toast that announced it, which is exactly what an Alert
 *     with `close={false}` does.
 *
 * `useScheduledPublication` owns the state and the modal so a single parent
 * can place the three surfaces independently.
 */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from '@byline/i18n/react';
import { Alert, Button, CloseIcon, DatePicker, IconButton, Label, Modal, Select, } from '@byline/ui/react';
import cx from 'clsx';
import styles from './scheduled-publication-control.module.css';
import { deriveScheduledPublicationState } from './scheduled-publication-state.js';
import { joinWallTime, resolveScheduledPublicationWallTime, wallTimeInZone, } from './scheduled-publication-time.js';
/** How often the armed → overdue boundary is re-checked while the editor sits open. */
const DUE_POLL_INTERVAL_MS = 30_000;
/** Default offset for a fresh schedule — far enough out to be reviewable. */
const DEFAULT_LEAD_MS = 15 * 60_000;
function browserTimeZone() {
    return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
}
/**
 * Render a wall time for display without turning it into an instant.
 *
 * The calendar day is formatted through a UTC-noon stand-in, which is safe
 * because only the day's name and number are taken from it; the clock reading
 * is appended verbatim, since the whole point is to show the reading the editor
 * chose rather than one a `Date` would have normalized it to.
 */
function formatWallTime(wall, locale) {
    const [year, month, day] = wall.date.split('-').map(Number);
    if (year == null || month == null || day == null) {
        return `${wall.date} ${wall.time}`;
    }
    if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) {
        return `${wall.date} ${wall.time}`;
    }
    const dayLabel = new Intl.DateTimeFormat(locale, {
        dateStyle: 'medium',
        timeZone: 'UTC',
    }).format(new Date(Date.UTC(year, month - 1, day, 12)));
    return `${dayLabel}, ${wall.time}`;
}
function seedScheduleInstant(schedule) {
    if (schedule != null)
        return new Date(schedule.publishAt);
    const seed = new Date(Date.now() + DEFAULT_LEAD_MS);
    seed.setSeconds(0, 0);
    return seed;
}
export function useScheduledPublication({ disabled = false, schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges, }) {
    const timeZone = useMemo(browserTimeZone, []);
    const [now, setNow] = useState(() => Date.now());
    const [showSchedule, setShowSchedule] = useState(false);
    const [busy, setBusy] = useState(false);
    // Only tick while something is actually scheduled — an editor with no
    // schedule has no boundary to cross, and a bare form should not re-render
    // on a timer.
    useEffect(() => {
        if (schedule == null)
            return;
        const timer = setInterval(() => setNow(Date.now()), DUE_POLL_INTERVAL_MS);
        return () => clearInterval(timer);
    }, [schedule]);
    const capabilities = useMemo(() => ({
        canSchedule: onSchedule != null,
        canConfirm: onConfirm != null,
        canCancel: onCancel != null,
    }), [onSchedule, onConfirm, onCancel]);
    const state = useMemo(() => deriveScheduledPublicationState(schedule, capabilities, now), [schedule, capabilities, now]);
    // Scheduling authorizes a specific reviewed version, so an unsaved edit has
    // to be resolved before any of these operations can name a version. Cancel
    // is exempt: withdrawing a schedule says nothing about content.
    const openSchedule = useCallback(() => {
        if (disabled)
            return;
        if (hasUnsavedChanges) {
            onUnsavedChanges();
            return;
        }
        setShowSchedule(true);
    }, [disabled, hasUnsavedChanges, onUnsavedChanges]);
    const confirm = useCallback(async () => {
        if (disabled || onConfirm == null)
            return;
        if (hasUnsavedChanges) {
            onUnsavedChanges();
            return;
        }
        setBusy(true);
        try {
            await onConfirm();
        }
        catch {
            // The host reports the failure; keep this view open.
        }
        finally {
            setBusy(false);
        }
    }, [disabled, onConfirm, hasUnsavedChanges, onUnsavedChanges]);
    const cancel = useCallback(async () => {
        if (disabled || onCancel == null)
            return;
        setBusy(true);
        try {
            await onCancel();
        }
        catch {
            // The host reports the failure; keep this view open.
        }
        finally {
            setBusy(false);
        }
    }, [disabled, onCancel]);
    const modal = showSchedule ? (<ScheduleModal schedule={schedule} timeZone={timeZone} onSubmit={async (input) => {
            if (disabled || onSchedule == null)
                return;
            setBusy(true);
            try {
                await onSchedule(input);
                setShowSchedule(false);
            }
            catch {
                // The host reports the failure; keep this view open.
            }
            finally {
                setBusy(false);
            }
        }} onDismiss={() => setShowSchedule(false)} busy={disabled || busy}/>) : null;
    return {
        state,
        timeZone,
        busy,
        isActive: state.kind !== 'none' || state.actions.schedule,
        openSchedule,
        confirm,
        cancel,
        modal,
    };
}
// ---------------------------------------------------------------------------
// Status-bar cell — the quiet, armed presentation
// ---------------------------------------------------------------------------
function useInstantFormatter(timeZone) {
    const { locale } = useTranslation('byline-admin');
    return useMemo(() => new Intl.DateTimeFormat(locale, {
        dateStyle: 'medium',
        timeStyle: 'short',
        timeZone,
    }), [locale, timeZone]);
}
/**
 * One metadata cell for the form's status bar, matching the Status /
 * Last modified / Created cells in scale, sitting immediately after the status
 * cell — an armed schedule says where the document is headed, which continues
 * what Status says about it rather than belonging with the timestamps.
 *
 * Written as a single phrase in the success colour rather than the label/value
 * pair its neighbours use: it is the one cell reporting something pending
 * rather than something already true, and it should read that way at a glance.
 * Only an armed schedule appears here; every other state is carried by the
 * notice instead, so the two never say the same thing twice.
 */
export function ScheduledPublicationCell({ state, timeZone, }) {
    const { t } = useTranslation('byline-admin');
    const format = useInstantFormatter(timeZone);
    if (state.kind !== 'armed' || state.publishAt == null)
        return null;
    return (<time className={cx('byline-scheduled-publication-cell', styles.cell)} dateTime={state.publishAt.toISOString()}>
      {t('scheduledPublication.status.cellText', {
            dateTime: `${format.format(state.publishAt)} (${timeZone})`,
        })}
    </time>);
}
// ---------------------------------------------------------------------------
// Notice — the escalated presentation for every exceptional state
// ---------------------------------------------------------------------------
/**
 * The durable notice for a suspended, overdue or failing schedule. Not
 * dismissible: `needs_reconfirm` has to stay on screen until an editor acts on
 * it, long after the toast that announced the suspension has gone.
 */
export function ScheduledPublicationNotice({ state, timeZone, busy, onConfirm, onReschedule, onCancel, }) {
    const { t } = useTranslation('byline-admin');
    const format = useInstantFormatter(timeZone);
    if (!state.isExceptional || state.publishAt == null)
        return null;
    const instant = `${format.format(state.publishAt)} (${timeZone})`;
    const title = state.kind === 'needs_reconfirm'
        ? t('scheduledPublication.status.needsReconfirm')
        : t('scheduledPublication.status.overdue');
    return (
    // A landmark rather than a live region: the toast already announces the
    // change assertively when it happens, and announcing the same thing twice
    // helps nobody. What the notice needs is to stay *findable* afterwards,
    // which a named region gives a screen-reader user.
    <div role="region" aria-label={title}>
      <Alert className={cx('byline-scheduled-publication-notice', styles.notice)} intent={state.tone === 'danger' ? 'danger' : 'warning'} icon={true} close={false} title={title}>
        <p className={styles['notice-body']}>
          {state.kind === 'needs_reconfirm'
            ? t('scheduledPublication.status.contentChanged')
            : t('scheduledPublication.status.overdueBody')}
        </p>
        <p className={styles['notice-instant']}>
          {t('scheduledPublication.status.authorizedFor', { dateTime: instant })}
          {state.kind === 'needs_reconfirm' && state.isPastDue && (<> {t('scheduledPublication.status.pastDueNote')}</>)}
        </p>
        {state.attemptCount > 0 && (<p className={styles['notice-attempts']}>
            {t('scheduledPublication.status.attempts', { count: state.attemptCount })}
          </p>)}
        {state.lastError != null && <p className={styles['notice-error']}>{state.lastError}</p>}
        <div className={cx('byline-scheduled-publication-notice-actions', styles['notice-actions'])}>
          {state.actions.confirm && (<Button size="sm" type="button" intent="success" disabled={busy} onClick={onConfirm}>
              {t('scheduledPublication.actions.confirm')}
            </Button>)}
          {state.actions.reschedule && (<Button size="sm" type="button" intent="info" disabled={busy} onClick={onReschedule}>
              {t('scheduledPublication.actions.reschedule')}
            </Button>)}
          {state.actions.cancel && (<Button size="sm" type="button" variant="text" disabled={busy} onClick={onCancel}>
              {t('scheduledPublication.actions.cancel')}
            </Button>)}
        </div>
      </Alert>
    </div>);
}
// ---------------------------------------------------------------------------
// Schedule / reschedule modal
// ---------------------------------------------------------------------------
/**
 * The picker's wall time is read, not its `Date`.
 *
 * `DatePicker` reports both: `onDateChange` gives an instant, and
 * `onWallTimeChange` gives the day and clock reading the editor actually
 * selected. Only the second one can express "02:30 on a day when 02:30 does not
 * exist" — the instant has already been normalized to 03:30 by then, and an
 * ambiguous 01:30 has already been resolved to the earlier of its two instants
 * without asking. So the wall time goes to
 * `resolveScheduledPublicationWallTime`, which is the only thing here allowed
 * to turn a wall time into an instant, and which refuses or asks as required.
 */
function ScheduleModal({ schedule, timeZone, onSubmit, onDismiss, busy, }) {
    const { t, locale } = useTranslation('byline-admin');
    // The instant the picker opens on. Always a real instant — either the one
    // already authorized, or a lead time from now — so seeding it never has to
    // construct a `Date` from a wall time.
    const [seedInstant] = useState(() => seedScheduleInstant(schedule));
    // Held, not recomputed per render, so the calendar's disabled range does not
    // shift under the editor while the modal is open.
    const [today] = useState(() => new Date());
    const [wall, setWall] = useState(() => wallTimeInZone(seedInstant, timeZone));
    const [instantChoices, setInstantChoices] = useState([]);
    const [selectedInstant, setSelectedInstant] = useState('');
    const [validationError, setValidationError] = useState(null);
    const resetResolution = () => {
        setInstantChoices([]);
        setSelectedInstant('');
        setValidationError(null);
    };
    const submit = async () => {
        const value = joinWallTime(wall);
        if (value == null) {
            setValidationError(t('scheduledPublication.form.invalid'));
            return;
        }
        const resolution = resolveScheduledPublicationWallTime(value, timeZone);
        if (resolution.status === 'invalid') {
            setValidationError(t('scheduledPublication.form.invalid'));
            return;
        }
        // Name the wall time in both daylight-saving messages. The picker's own
        // field cannot be trusted to show it: having normalized the selection, it
        // displays 03:30 for a 02:30 that does not exist, so a message that just
        // said "that time" would appear to be rejecting the time on screen.
        const offending = formatWallTime(wall, locale);
        if (resolution.status === 'nonexistent') {
            setValidationError(t('scheduledPublication.form.nonexistent', { wallTime: offending }));
            return;
        }
        if (resolution.choices.length > 1 &&
            !resolution.choices.some((c) => c.iso === selectedInstant)) {
            setInstantChoices(resolution.choices);
            setValidationError(t('scheduledPublication.form.ambiguous', { wallTime: offending }));
            return;
        }
        const publishAtIso = selectedInstant || resolution.choices[0]?.iso;
        if (publishAtIso == null)
            return;
        // Checked on the resolved instant rather than the wall time, because an
        // ambiguous overlap has two instants an hour apart and only one of them may
        // still be ahead.
        //
        // This is an affordance, not the guarantee. The server compares against
        // database time and refuses `publish_at_not_future` regardless of what the
        // browser's clock believes — catching it here just replaces a raw server
        // error in a danger toast with a message next to the field that caused it.
        if (Date.parse(publishAtIso) <= Date.now()) {
            setValidationError(t('scheduledPublication.form.notFuture', { wallTime: offending }));
            return;
        }
        await onSubmit({ publishAt: publishAtIso });
    };
    const title = schedule == null
        ? t('scheduledPublication.form.scheduleTitle')
        : t('scheduledPublication.form.rescheduleTitle');
    return (<Modal isOpen closeOnOverlayClick={!busy} onDismiss={() => {
            if (!busy)
                onDismiss();
        }}>
      <Modal.Container style={{ maxWidth: '560px' }}>
        <Modal.Header className={styles.modalHead}>
          <h3 className={styles.modalTitle}>{title}</h3>
          <IconButton aria-label={t('common.actions.close')} size="xs" disabled={busy} onClick={onDismiss}>
            <CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
          </IconButton>
        </Modal.Header>
        <Modal.Content>
          <div className={styles.field}>
            <DatePicker id="scheduled-publication-at" name="scheduled-publication-at" label={t('scheduledPublication.form.dateTime')} mode="datetime" inputSize="sm" 
    // Today is the earliest selectable day. Deliberately day-grained:
    // a later slot today is a perfectly good schedule, so the day
    // stays open and the submit-time check below is what rejects an
    // hour that has already gone.
    minDate={today} yearsInPast={0} yearsInFuture={5} initialValue={seedInstant} onWallTimeChange={(value) => {
            if (value == null)
                return;
            setWall(value);
            resetResolution();
        }}/>
          </div>
          <p className={styles.help}>{t('scheduledPublication.form.timeZone', { timeZone })}</p>
          {instantChoices.length > 1 && (<div className={styles.choice}>
              <Label id="scheduled-publication-offset-label" htmlFor="scheduled-publication-offset" label={t('scheduledPublication.form.offset')}/>
              <Select id="scheduled-publication-offset" name="scheduled-publication-offset" ariaLabel={t('scheduledPublication.form.offset')} containerClassName={styles['choice-select']} placeholder={t('scheduledPublication.form.offsetPlaceholder')} size="sm" value={selectedInstant} 
        // The resolver returns the overlap's instants in chronological
        // order, so the two can be named by when they happen. A bare
        // "UTC-04:00" asks the editor to know which side of the
        // transition that is; "Earlier" and "Later" do not.
        items={instantChoices.map((choice, index) => ({
                value: choice.iso,
                label: t(index === 0
                    ? 'scheduledPublication.form.offsetEarlier'
                    : 'scheduledPublication.form.offsetLater', { offset: choice.offsetLabel }),
            }))} onValueChange={(value) => {
                setSelectedInstant(value ?? '');
                setValidationError(null);
            }} disabled={busy}/>
            </div>)}
          {validationError != null && (<p className={styles.validation} role="alert">
              {validationError}
            </p>)}
          <p className={styles.warningText}>{t('scheduledPublication.form.editWarning')}</p>
        </Modal.Content>
        <Modal.Actions>
          <Button size="sm" intent="noeffect" disabled={busy} onClick={onDismiss}>
            {t('common.actions.cancel')}
          </Button>
          <Button size="sm" intent="primary" disabled={busy || wall.date.length === 0 || wall.time.length === 0} onClick={submit}>
            {busy
            ? t('scheduledPublication.form.saving')
            : schedule == null
                ? t('scheduledPublication.actions.schedule')
                : t('scheduledPublication.actions.reschedule')}
          </Button>
        </Modal.Actions>
      </Modal.Container>
    </Modal>);
}
