import React, { useEffect, useMemo, useState } from 'react';
import { FaArrowDown, FaArrowUp, FaTimes } from 'react-icons/fa';

import type { ArticleSummary } from '../../service/visibility/visibility.interface';
import Datepicker from '../ui/Datepicker';

import Modal from './Modal';

/** Hour of day the queue publishes at when the merchant only picks a date. */
const DEFAULT_PUBLISH_HOUR = 9;
/** Most articles one queue may hold; mirrors the agent's cap. */
const MAX_QUEUE_ARTICLES = 22;
/** Saturday in ``Date.getDay()`` terms (Sunday is 0). */
const SATURDAY_INDEX = 6;
/** Sunday in ``Date.getDay()`` terms. */
const SUNDAY_INDEX = 0;

/** Props for {@link SchedulePublishModal}. */
interface SchedulePublishModalProps {
  open: boolean;
  onOpenChange: (next: boolean) => void;
  /** The articles the merchant picked, in the order they were selected. */
  articles: ArticleSummary[];
  /** Disable the controls while the queue is being created. */
  busy?: boolean;
  /**
   * Days already chosen, parallel to ``articles``. Passed when editing an
   * existing queue entry so the calendar opens on the day the article is set
   * to, instead of proposing a fresh one.
   */
  initialDates?: string[];
  /** Overrides the confirm label, e.g. when changing an existing day. */
  confirmLabel?: string;
  /**
   * Why the last attempt failed. Rendered INSIDE the modal, next to the
   * confirm button: this overlay covers the page, so anything reported behind
   * it stays invisible while the merchant is still looking at what failed.
   */
  error?: string | null;
  /** Fired with the final order and the chosen day for each article. */
  onConfirm: (articleIds: string[], publishDates: string[]) => void;
}

/**
 * Move a date to the next weekday when it lands on a Saturday or Sunday.
 *
 * @param date {Date} The candidate day.
 * @returns {Date} The same day, or the following Monday.
 */
const skipWeekend = (date: Date): Date => {
  const moved = new Date(date);
  while (moved.getDay() === SATURDAY_INDEX || moved.getDay() === SUNDAY_INDEX) {
    moved.setDate(moved.getDate() + 1);
  }

  return moved;
};

/**
 * Build one publishing day per article, starting tomorrow.
 *
 * @param count {number} How many days to produce.
 * @param weekdaysOnly {boolean} Skip Saturday and Sunday.
 * @returns {Date[]} The days, in order.
 */
const buildDefaultDays = (count: number, weekdaysOnly: boolean): Date[] => {
  const days: Date[] = [];
  const cursor = new Date();
  cursor.setHours(DEFAULT_PUBLISH_HOUR, 0, 0, 0);
  cursor.setDate(cursor.getDate() + 1);
  for (let index = 0; index < count; index += 1) {
    const day = weekdaysOnly ? skipWeekend(cursor) : new Date(cursor);
    days.push(new Date(day));
    cursor.setTime(day.getTime());
    cursor.setDate(cursor.getDate() + 1);
  }

  return days;
};

/**
 * Bulk-schedule reviewed articles: order them, give each one a day, and let
 * the queue publish them live so the merchant never has to come back and
 * click publish every morning.
 *
 * @param props {SchedulePublishModalProps} Modal props.
 * @returns {JSX.Element} The modal.
 */
const SchedulePublishModal = ({
  open,
  onOpenChange,
  articles,
  busy = false,
  initialDates,
  confirmLabel,
  error,
  onConfirm,
}: SchedulePublishModalProps): JSX.Element => {
  const [ordered, setOrdered] = useState<ArticleSummary[]>([]);
  const [days, setDays] = useState<Date[]>([]);
  const [weekdaysOnly, setWeekdaysOnly] = useState(true);

  // The parent rebuilds the ``articles`` array on every render, so the seed
  // keys off the ids instead - otherwise a re-render while the modal is open
  // would throw away the ordering the merchant just arranged. ``initialDates``
  // is keyed the same way, for the same reason.
  const pickedIds = articles.map(article => article.article_id).join(',');
  const pickedDates = (initialDates ?? []).join(',');

  // Re-seed every time the modal opens so a previous selection never leaks in.
  useEffect(() => {
    if (!open) return;
    const picked = articles.slice(0, MAX_QUEUE_ARTICLES);
    setOrdered(picked);
    // Editing an existing queue entry: open on the day it is already set to.
    // A partial list of days would pair articles with the wrong dates, so it
    // only seeds when there is exactly one day per article.
    const seeded = (initialDates ?? [])
      .slice(0, picked.length)
      .map(one => new Date(one));
    setDays(
      seeded.length === picked.length
        ? seeded
        : buildDefaultDays(picked.length, true)
    );
    setWeekdaysOnly(true);
  }, [open, pickedIds, pickedDates]);

  // Today, not tomorrow: an article already queued for today has to be able to
  // show the day it is set to, and the calendar would otherwise refuse the day
  // it was just seeded with.
  const earliestDay = useMemo(() => {
    const date = new Date();
    date.setHours(0, 0, 0, 0);

    return date;
  }, []);

  // Editing the day of one article already in a queue, rather than laying out
  // a fresh batch. The batch-only controls are hidden in that mode.
  const editingOneDay = Boolean(initialDates);

  /**
   * Swap an article with its neighbour so the merchant can decide which one
   * goes live first.
   *
   * @param index {number} Position of the article being moved.
   * @param direction {-1 | 1} -1 moves it earlier, 1 moves it later.
   * @returns {void}
   */
  const move = (index: number, direction: -1 | 1): void => {
    const target = index + direction;
    if (target < 0 || target >= ordered.length) return;
    const nextOrdered = [...ordered];
    [nextOrdered[index], nextOrdered[target]] = [
      nextOrdered[target],
      nextOrdered[index],
    ];
    setOrdered(nextOrdered);
  };

  /**
   * Drop one article from the queue without leaving the modal. The remaining
   * articles keep the days already assigned to their positions.
   *
   * @param index {number} Position of the article being dropped.
   * @returns {void}
   */
  const removeAt = (index: number): void => {
    setOrdered(ordered.filter((_, position) => position !== index));
    setDays(days.filter((_, position) => position !== index));
  };

  /**
   * Pin one article to a specific day. The publishing hour is fixed, so only
   * the date part of the pick matters.
   *
   * @param index {number} Position of the article being dated.
   * @param date {Date | undefined} The day picked in the calendar.
   * @returns {void}
   */
  const setDay = (index: number, date?: Date): void => {
    if (!date) return;
    const nextDays = [...days];
    const withHour = new Date(date);
    withHour.setHours(DEFAULT_PUBLISH_HOUR, 0, 0, 0);
    nextDays[index] = withHour;
    setDays(nextDays);
  };

  /**
   * Flip the weekday switch and lay the whole batch out again from tomorrow,
   * since keeping half-manual dates around would only confuse the order.
   *
   * @param nextWeekdaysOnly {boolean} Whether weekends must be skipped.
   * @returns {void}
   */
  const respread = (nextWeekdaysOnly: boolean): void => {
    setWeekdaysOnly(nextWeekdaysOnly);
    setDays(buildDefaultDays(ordered.length, nextWeekdaysOnly));
  };

  const handleConfirm = (): void => {
    onConfirm(
      ordered.map(article => article.article_id),
      ordered.map((_, index) => (days[index] ?? new Date()).toISOString())
    );
  };

  return (
    <Modal
      open={open}
      onClose={() => onOpenChange(false)}
      title="Schedule publishing"
      size="lg"
      footer={
        <>
          {error && (
            <p role="alert" className="mr-auto text-sm text-red-700">
              {error}
            </p>
          )}
          <button
            type="button"
            onClick={() => onOpenChange(false)}
            disabled={busy}
            className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-60"
          >
            Cancel
          </button>
          <button
            type="button"
            onClick={handleConfirm}
            disabled={busy || ordered.length === 0}
            className="rounded-md bg-black px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-800 disabled:opacity-60"
          >
            {busy
              ? 'Scheduling…'
              : (confirmLabel ??
                `Schedule ${ordered.length} article${
                  ordered.length === 1 ? '' : 's'
                }`)}
          </button>
        </>
      }
    >
      <div className="flex flex-col gap-3">
        <p className="text-sm text-gray-600">
          Pick the day each article goes live. They publish by themselves, in
          this order, so you only review once (always live, never as a draft).
        </p>

        {/* The spreader rebuilds every day from scratch, so in the
            single-article edit flow it could only ever discard the very day
            being edited. */}
        {!editingOneDay && (
          <div className="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
            <span className="flex flex-col gap-0.5">
              <span className="text-sm font-medium text-gray-900">
                Weekdays only
              </span>
              <span className="text-xs text-gray-500">
                Spread them Monday to Friday and skip the weekend.
              </span>
            </span>
            <button
              type="button"
              role="switch"
              aria-checked={weekdaysOnly}
              aria-label="Spread over weekdays only"
              disabled={busy}
              onClick={() => respread(!weekdaysOnly)}
              className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors disabled:opacity-60 ${
                weekdaysOnly ? 'bg-black' : 'bg-gray-300'
              }`}
            >
              <span
                aria-hidden="true"
                className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
                  weekdaysOnly ? 'translate-x-6' : 'translate-x-1'
                }`}
              />
            </button>
          </div>
        )}

        {ordered.length === 0 ? (
          <p className="py-6 text-center text-sm text-gray-500">
            Nothing selected. Pick the articles you want to schedule.
          </p>
        ) : (
          <ul className="flex flex-col gap-2">
            {ordered.map((article, index) => {
              const label = article.title || article.article_id;

              return (
                <li
                  key={article.article_id}
                  className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2"
                >
                  <span className="w-6 flex-shrink-0 text-center text-sm font-semibold text-gray-500">
                    {index + 1}
                  </span>
                  <span className="min-w-0 flex-1 truncate text-sm text-gray-900">
                    {label}
                  </span>
                  <div className="w-44 flex-shrink-0">
                    <Datepicker
                      align="right"
                      mode="single"
                      value={days[index]}
                      minDate={earliestDay}
                      staticCalendar={false}
                      ariaLabel={`Publishing day for ${label}`}
                      onChange={dates => setDay(index, dates[0])}
                    />
                  </div>
                  <div className="flex flex-shrink-0 items-center gap-1">
                    <button
                      type="button"
                      aria-label={`Move ${label} earlier`}
                      title="Move earlier"
                      disabled={busy || index === 0}
                      onClick={() => move(index, -1)}
                      className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-40"
                    >
                      <FaArrowUp aria-hidden="true" size={12} />
                    </button>
                    <button
                      type="button"
                      aria-label={`Move ${label} later`}
                      title="Move later"
                      disabled={busy || index === ordered.length - 1}
                      onClick={() => move(index, 1)}
                      className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-gray-200 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-40"
                    >
                      <FaArrowDown aria-hidden="true" size={12} />
                    </button>
                    {/* Dropping the only row of a single-article edit reads
                        as "unschedule this article", which it never was: it
                        just empties the modal. */}
                    {!editingOneDay && (
                      <button
                        type="button"
                        aria-label={`Remove ${label} from the queue`}
                        title="Remove from the queue"
                        disabled={busy}
                        onClick={() => removeAt(index)}
                        className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-red-200 bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700 disabled:opacity-40"
                      >
                        <FaTimes aria-hidden="true" size={12} />
                      </button>
                    )}
                  </div>
                </li>
              );
            })}
          </ul>
        )}

        {articles.length > MAX_QUEUE_ARTICLES && (
          <p className="text-sm text-gray-600">
            {`Only the first ${MAX_QUEUE_ARTICLES} can go in one queue. The other ${
              articles.length - MAX_QUEUE_ARTICLES
            } stay selected, so you can queue them straight after.`}
          </p>
        )}
      </div>
    </Modal>
  );
};

export default SchedulePublishModal;
