import { useState } from 'react';
import { api } from '../api/client';
import { useAction, useQuery } from '../lib/hooks';
import { useToast } from '../lib/toast';
import { ago, duration, interval, num, when } from '../lib/format';
import {
  Banner,
  Button,
  Card,
  EmptyRow,
  Loading,
  Note,
  PageHead,
  TableWrap,
  Tag,
  Tiles,
  Toggle,
} from '../components/ui';
import type { ScheduledTask, TaskRun, TasksResponse } from '../api/types';
import type { Tone } from '../lib/format';

/* Scheduled tasks: what the gateway does when nobody is watching.
 *
 * Polled faster than the console's own heartbeat while a task is running, because the one
 * thing an operator does here is press Run now and then watch for the outcome — and a
 * thirty-second poll makes a two-second job look like one that did nothing. */

const IDLE_POLL_MS = 20_000;
const BUSY_POLL_MS = 3_000;

/* The cadences an operator may choose from.
 *
 * A fixed list rather than a free-text duration, because the useful range here spans three
 * orders of magnitude and the two ways to get it wrong are both silent: a number typed in
 * the wrong unit, and a cadence so tight the job never finishes before it is due again. The
 * floor matches the scheduler's own — it clamps anything under a minute — so the console
 * cannot offer a value the server would quietly change underneath it.
 *
 * Zero is absent on purpose. The API reads it as "restore the declared cadence" rather than
 * as "never", so a "run by hand only" entry here would appear to do nothing on any task that
 * declares an interval. That is a server-side limitation and it belongs in the server, not
 * in a control that lies about it. */
const CADENCE_CHOICES: { value: number; label: string }[] = [
  { value: 60, label: 'Every minute' },
  { value: 300, label: 'Every 5 minutes' },
  { value: 600, label: 'Every 10 minutes' },
  { value: 900, label: 'Every 15 minutes' },
  { value: 1_800, label: 'Every 30 minutes' },
  { value: 3_600, label: 'Hourly' },
  { value: 10_800, label: 'Every 3 hours' },
  { value: 21_600, label: 'Every 6 hours' },
  { value: 43_200, label: 'Every 12 hours' },
  { value: 86_400, label: 'Daily' },
  { value: 604_800, label: 'Weekly' },
];

/* The choices for one task: the presets, plus its own declared cadence and whatever it is
 * currently set to if either falls outside the list.
 *
 * Adding them rather than snapping to the nearest preset is what stops the control being
 * destructive to look at — a task declaring 45 minutes must not silently become hourly
 * because somebody opened the page and the select had to show *something*. */
function cadenceChoices(task: ScheduledTask): { value: number; label: string }[] {
  const choices = [...CADENCE_CHOICES];
  for (const seconds of [task.defaultIntervalSeconds, task.intervalSeconds]) {
    if (seconds > 0 && !choices.some((choice) => choice.value === seconds)) {
      choices.push({ value: seconds, label: interval(seconds).replace(/^every /, 'Every ') });
    }
  }
  return choices.sort((a, b) => a.value - b.value);
}

function statusTone(status: TaskRun['status']): Tone {
  if (status === 'failed') return 'bad';
  if (status === 'running') return 'info';
  if (status === 'skipped') return 'warn';
  return 'ok';
}

export function TasksPage() {
  const { wrap } = useToast();
  const { busy, run } = useAction();
  const [fast, setFast] = useState(false);
  const { data, error, loading, reload } = useQuery<TasksResponse>('/admin/api/tasks?limit=60', {
    pollMs: fast ? BUSY_POLL_MS : IDLE_POLL_MS,
  });

  const tasks = data?.tasks ?? [];
  const anyRunning = tasks.some((task) => task.running);
  if (anyRunning !== fast) setFast(anyRunning);

  const runNow = (task: ScheduledTask) =>
    run(task.id, async () => {
      await wrap(
        () => api.post(`/admin/api/tasks/${encodeURIComponent(task.id)}/run`),
        `${task.name} started.`,
      );
      await reload();
    });

  const setEnabled = (task: ScheduledTask, enabled: boolean) =>
    run(`${task.id}:enabled`, async () => {
      await wrap(
        () => api.put(`/admin/api/tasks/${encodeURIComponent(task.id)}`, { enabled }),
        enabled ? `${task.name} switched on.` : `${task.name} switched off.`,
      );
      await reload();
    });

  // Named setCadence rather than setInterval so it cannot shadow the global of that
  // name inside this component, which is a trap for anything added here later.
  const setCadence = (task: ScheduledTask, intervalSeconds: number) =>
    run(`${task.id}:interval`, async () => {
      await wrap(
        () => api.put(`/admin/api/tasks/${encodeURIComponent(task.id)}`, { intervalSeconds }),
        `${task.name} now runs ${interval(intervalSeconds)}.`,
      );
      await reload();
    });

  // Sending zero is how the API is told to forget an override, so this is a separate call
  // from the select rather than an option inside it — see CADENCE_CHOICES.
  const resetCadence = (task: ScheduledTask) =>
    run(`${task.id}:interval`, async () => {
      await wrap(
        () => api.put(`/admin/api/tasks/${encodeURIComponent(task.id)}`, { intervalSeconds: 0 }),
        `${task.name} back to its default cadence.`,
      );
      await reload();
    });

  const failures = tasks.filter((task) => task.lastRun?.status === 'failed').length;
  const retimed = tasks.filter(
    (task) => task.defaultIntervalSeconds > 0 && task.intervalSeconds !== task.defaultIntervalSeconds,
  ).length;
  const disabled = tasks.filter((task) => !task.enabled).length;

  const groups = data?.groups ?? [];
  const ungrouped = tasks.filter((task) => !task.group);

  return (
    <>
      <PageHead
        title="Scheduled tasks"
        intro="The gateway's background work: what it does, when it last ran, how long it took and whether it worked. Every one of these can be started by hand."
      />

      <Banner message={error} />

      {loading ? (
        <Loading />
      ) : (
        <>
          <Tiles
            tiles={[
              { label: 'Tasks', value: num(tasks.length), icon: 'clock', tone: 'info' },
              {
                label: 'Running now',
                value: num(tasks.filter((task) => task.running).length),
                icon: 'pulse',
                tone: anyRunning ? 'ok' : undefined,
              },
              {
                label: 'Last run failed',
                value: num(failures),
                icon: 'alert',
                tone: failures > 0 ? 'bad' : undefined,
              },
              {
                label: 'Switched off',
                value: num(disabled),
                icon: 'power',
                tone: disabled > 0 ? 'warn' : undefined,
              },
              // Worth a tile of its own: a retimed task is the most likely explanation for
              // "why has this not run", and it is invisible on a page that only prints the
              // cadence currently in force.
              {
                label: 'Retimed',
                value: num(retimed),
                icon: 'clock',
                tone: retimed > 0 ? 'note' : undefined,
              },
            ]}
          />

          {failures > 0 ? (
            <Note tone="bad">
              A failed task publishes an administrative event, so the failure is in the activity feed and
              wherever your integrations send it — you did not have to be looking at this page.
            </Note>
          ) : null}

          {[...groups, ...(ungrouped.length > 0 ? [''] : [])].map((group) => {
            const inGroup = tasks.filter((task) => task.group === group);
            if (inGroup.length === 0) return null;
            return (
              <Card
                key={group || 'other'}
                title={group || 'Other'}
                icon={group === 'System' ? 'chip' : group === 'Analytics' ? 'chart' : 'wrench'}
                tone={group === 'System' ? 'info' : group === 'Analytics' ? 'data' : 'note'}
              >
                <div className="list">
                  {inGroup.map((task) => (
                    <div className="list-item" key={task.id}>
                      <div className="list-body">
                        <b>
                          {task.name}{' '}
                          {task.running ? <Tag tone="info">running</Tag> : null}
                          {!task.enabled ? <Tag tone="warn">off</Tag> : null}
                          {task.defaultIntervalSeconds > 0 &&
                          task.intervalSeconds !== task.defaultIntervalSeconds ? (
                            <Tag tone="note">retimed</Tag>
                          ) : null}
                        </b>
                        <p>{task.description}</p>
                        <p className="quiet">
                          {interval(task.intervalSeconds)}
                          {task.enabled && task.nextRun ? ` · next ${ago(task.nextRun).replace(' ago', '')}` : ''}
                          {task.lastRun ? (
                            <>
                              {' · last '}
                              <span title={when(task.lastRun.startedAt)}>{ago(task.lastRun.startedAt)}</span>
                              {` in ${duration(task.lastRun.durationMs)}`}
                              {task.lastRun.detail ? ` — ${task.lastRun.detail}` : ''}
                            </>
                          ) : (
                            ' · never run'
                          )}
                        </p>
                        {task.lastRun?.error ? (
                          <p className="mono" style={undefined}>
                            <Tag tone="bad">{task.lastRun.error}</Tag>
                          </p>
                        ) : null}
                      </div>
                      <div className="list-actions">
                        {task.lastRun ? (
                          <Tag tone={statusTone(task.lastRun.status)}>{task.lastRun.status}</Tag>
                        ) : (
                          <Tag>never run</Tag>
                        )}
                        {/* Disabled while a run is in flight: changing the cadence
                            reschedules from now, and doing that underneath a running job
                            is how one run silently becomes two. */}
                        <select
                          aria-label={`How often ${task.name} runs`}
                          value={task.intervalSeconds}
                          disabled={busy === `${task.id}:interval` || task.running}
                          onChange={(event) => void setCadence(task, Number(event.target.value))}
                        >
                          {cadenceChoices(task).map((choice) => (
                            <option key={choice.value} value={choice.value}>
                              {choice.label}
                              {choice.value === task.defaultIntervalSeconds ? ' (default)' : ''}
                            </option>
                          ))}
                        </select>
                        {task.defaultIntervalSeconds > 0 &&
                        task.intervalSeconds !== task.defaultIntervalSeconds ? (
                          <Button
                            size="sm"
                            icon="refresh"
                            busy={busy === `${task.id}:interval`}
                            onClick={() => void resetCadence(task)}
                          >
                            Default
                          </Button>
                        ) : null}
                        <Toggle
                          label=""
                          checked={task.enabled}
                          disabled={busy === `${task.id}:enabled`}
                          onChange={(next) => void setEnabled(task, next)}
                        />
                        <Button
                          size="sm"
                          icon="play"
                          busy={busy === task.id}
                          disabled={task.running}
                          onClick={() => void runNow(task)}
                        >
                          Run now
                        </Button>
                      </div>
                    </div>
                  ))}
                </div>
              </Card>
            );
          })}

          <Card
            title="Recent runs"
            intro="Every task together and in order, which is what shows two jobs interfering with each other."
            icon="history"
            tone="note"
          >
            <TableWrap>
              <table>
                <thead>
                  <tr>
                    <th className="nowrap">Started</th>
                    <th>Task</th>
                    <th>Trigger</th>
                    <th>Result</th>
                    <th className="num">Took</th>
                    <th>Detail</th>
                  </tr>
                </thead>
                <tbody>
                  {(data?.runs.length ?? 0) === 0 ? (
                    <EmptyRow columns={6}>No task has run yet.</EmptyRow>
                  ) : (
                    data?.runs.map((entry) => (
                      <tr key={entry.id}>
                        <td className="nowrap muted" title={when(entry.startedAt)}>
                          {ago(entry.startedAt)}
                        </td>
                        <td>{tasks.find((task) => task.id === entry.taskId)?.name ?? entry.taskId}</td>
                        <td className="muted">{entry.trigger}</td>
                        <td>
                          <Tag tone={statusTone(entry.status)}>{entry.status}</Tag>
                        </td>
                        <td className="num muted">{duration(entry.durationMs)}</td>
                        <td className="muted">{entry.error || entry.detail || '—'}</td>
                      </tr>
                    ))
                  )}
                </tbody>
              </table>
            </TableWrap>
          </Card>
        </>
      )}
    </>
  );
}
