import { useState } from 'react';
import { api } from '../api/client';
import { useAction, useQuery } from '../lib/hooks';
import { useToast } from '../lib/toast';
import { ago, duration, num, when } from '../lib/format';
import {
  Banner,
  Button,
  Card,
  Confirm,
  Empty,
  EmptyRow,
  Field,
  Loading,
  Note,
  PageHead,
  TableWrap,
  Tag,
  Toggle,
} from '../components/ui';
import type { Integration, IntegrationEventOption, IntegrationsResponse } from '../api/types';

/* Integrations: administrative events going out to somewhere else.
 *
 * The whole page is written around one property of the backend, and it is worth stating
 * because the form would otherwise look careless: the webhook address is never returned.
 * It is the credential — anybody holding it can post into the channel — so the gateway
 * sends back only whether one is set and the channel id from the middle of it. That is why
 * the address field on an existing integration is blank with a placeholder saying so, and
 * why saving with it blank leaves the stored one alone. */

interface Draft {
  id: string;
  name: string;
  url: string;
  enabled: boolean;
  events: string[];
}

const NEW_DRAFT: Draft = { id: '', name: 'Discord', url: '', enabled: true, events: [] };

export function IntegrationsPage() {
  const { wrap, show } = useToast();
  const { busy, run } = useAction();
  const { data, error, loading, reload } = useQuery<IntegrationsResponse>('/admin/api/integrations', {
    pollMs: 60_000,
  });
  const [draft, setDraft] = useState<Draft | null>(null);
  const [confirming, setConfirming] = useState<Integration | null>(null);

  const catalogue = data?.catalogue ?? [];
  const integrations = data?.integrations ?? [];

  const edit = (integration: Integration) =>
    setDraft({
      id: integration.id,
      name: integration.name,
      url: '',
      enabled: integration.enabled,
      events: integration.events ?? [],
    });

  const save = () =>
    run('save', async () => {
      if (!draft) return;
      const saved = await wrap(
        () => api.post<IntegrationsResponse>('/admin/api/integrations', draft),
        draft.id ? 'Integration saved.' : 'Integration added.',
      );
      if (saved) {
        setDraft(null);
        await reload();
      }
    });

  const remove = (integration: Integration) =>
    run('remove', async () => {
      await wrap(
        () => api.del(`/admin/api/integrations/${encodeURIComponent(integration.id)}`),
        `${integration.name} removed.`,
      );
      setConfirming(null);
      await reload();
    });

  const test = (integration: Integration) =>
    run(`test:${integration.id}`, async () => {
      const result = await wrap(() =>
        api.post<{ ok: boolean; message: string }>(
          `/admin/api/integrations/${encodeURIComponent(integration.id)}/test`,
        ),
      );
      // The test route answers 200 whether or not the webhook accepted it, because the
      // *request* succeeded — so the verdict is in the body, and reporting it is this
      // page's job rather than the transport's.
      if (result) show(result.message, result.ok ? 'ok' : 'bad');
      await reload();
    });

  return (
    <>
      <PageHead
        title="Integrations"
        intro="Send administrative events to somewhere you already look. Events pass through the gateway's own event layer, so nothing about authentication or scheduled tasks knows Discord exists — and a second kind of destination is a change here rather than everywhere."
        actions={
          <Button variant="primary" icon="plus" onClick={() => setDraft(NEW_DRAFT)}>
            Add a webhook
          </Button>
        }
      />

      <Banner message={error} />

      {(data?.dropped ?? 0) > 0 ? (
        <Note tone="warn">
          {num(data?.dropped ?? 0)} events could not be queued for delivery. The queue is deliberately
          lossy — a slow endpoint must never hold up a television signing in — but a number growing here
          means a destination is not keeping up.
        </Note>
      ) : null}

      {loading ? (
        <Loading />
      ) : integrations.length === 0 && !draft ? (
        <Card title="Nothing configured" icon="plug" tone="note">
          <Empty>
            No destinations yet. A Discord webhook takes about a minute: in Discord, open a channel's
            settings → Integrations → Webhooks → New Webhook, copy its URL, and paste it here.
          </Empty>
        </Card>
      ) : (
        integrations.map((integration) => (
          <IntegrationCard
            key={integration.id}
            integration={integration}
            catalogue={catalogue}
            busy={busy}
            onEdit={() => edit(integration)}
            onTest={() => void test(integration)}
            onRemove={() => setConfirming(integration)}
          />
        ))
      )}

      {draft ? (
        <DraftCard
          draft={draft}
          catalogue={catalogue}
          busy={busy === 'save'}
          onChange={setDraft}
          onSave={() => void save()}
          onCancel={() => setDraft(null)}
        />
      ) : null}

      {confirming ? (
        <Confirm
          title={`Remove ${confirming.name}?`}
          body="The webhook address and its delivery history go with it. Events already published stay in the activity feed."
          confirmLabel="Remove"
          destructive
          busy={busy === 'remove'}
          onConfirm={() => void remove(confirming)}
          onCancel={() => setConfirming(null)}
        />
      ) : null}
    </>
  );
}

function IntegrationCard({
  integration,
  catalogue,
  busy,
  onEdit,
  onTest,
  onRemove,
}: {
  integration: Integration;
  catalogue: IntegrationEventOption[];
  busy: string | null;
  onEdit: () => void;
  onTest: () => void;
  onRemove: () => void;
}) {
  const health = integration.health;
  // "Is it working" is answered by the *last* attempt, not by a failure count: a webhook
  // that failed once an hour ago and has worked since is healthy.
  const healthy =
    !health.lastFailure || (health.lastSuccess && health.lastSuccess > health.lastFailure);
  const selected = integration.events ?? [];

  return (
    <Card
      title={integration.name}
      intro={integration.hint ? `Discord webhook ${integration.hint}` : 'Discord webhook'}
      icon="plug"
      tone={integration.enabled ? 'ok' : 'warn'}
      actions={
        <>
          {integration.enabled ? <Tag tone="ok">on</Tag> : <Tag tone="warn">off</Tag>}
          {health.deliveries > 0 ? (
            <Tag tone={healthy ? 'ok' : 'bad'}>{healthy ? 'delivering' : 'failing'}</Tag>
          ) : (
            <Tag>never used</Tag>
          )}
          <Button size="sm" icon="pulse" busy={busy === `test:${integration.id}`} onClick={onTest}>
            Test
          </Button>
          <Button size="sm" onClick={onEdit}>
            Edit
          </Button>
          <Button size="sm" variant="danger" icon="trash" onClick={onRemove} title="Remove" />
        </>
      }
    >
      <div className="list">
        <div className="list-item">
          <div className="list-body">
            <b>Events sent</b>
            <p>
              {selected.length === 0
                ? 'None selected — this destination is configured but will never post anything.'
                : selected
                    .map((type) => catalogue.find((entry) => entry.type === type)?.label ?? type)
                    .join(', ')}
            </p>
          </div>
        </div>
        <div className="list-item">
          <div className="list-body">
            <b>Last delivered</b>
            <p>{health.lastSuccess ? when(health.lastSuccess) : 'never'}</p>
          </div>
          <div className="list-actions">
            {health.deliveries > 0 ? (
              <span className="quiet">
                {num(health.deliveries)} attempts, {num(health.failures)} failed
              </span>
            ) : null}
          </div>
        </div>
        {health.lastFailure ? (
          <div className="list-item">
            <div className="list-body">
              <b>Last failure</b>
              <p>
                {when(health.lastFailure)}
                {health.lastError ? ` — ${health.lastError}` : ''}
              </p>
            </div>
          </div>
        ) : null}
      </div>

      {integration.deliveries.length > 0 ? (
        <TableWrap>
          <table>
            <thead>
              <tr>
                <th className="nowrap">Attempted</th>
                <th>Event</th>
                <th>Result</th>
                <th className="num">Took</th>
              </tr>
            </thead>
            <tbody>
              {integration.deliveries.map((delivery) => (
                <tr key={delivery.id}>
                  <td className="nowrap muted" title={when(delivery.attemptedAt)}>
                    {ago(delivery.attemptedAt)}
                  </td>
                  <td className="muted">{delivery.eventType}</td>
                  <td>
                    {delivery.success ? (
                      <Tag tone="ok">{delivery.statusCode || 'ok'}</Tag>
                    ) : (
                      <Tag tone="bad">{delivery.error || delivery.statusCode || 'failed'}</Tag>
                    )}
                  </td>
                  <td className="num muted">{duration(delivery.durationMs)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </TableWrap>
      ) : (
        <TableWrap>
          <table>
            <tbody>
              <EmptyRow columns={4}>Nothing has been delivered through this webhook yet.</EmptyRow>
            </tbody>
          </table>
        </TableWrap>
      )}
    </Card>
  );
}

function DraftCard({
  draft,
  catalogue,
  busy,
  onChange,
  onSave,
  onCancel,
}: {
  draft: Draft;
  catalogue: IntegrationEventOption[];
  busy: boolean;
  onChange: (next: Draft) => void;
  onSave: () => void;
  onCancel: () => void;
}) {
  const groups = [...new Set(catalogue.map((entry) => entry.group))];
  const toggleEvent = (type: string, on: boolean) =>
    onChange({
      ...draft,
      events: on ? [...draft.events, type] : draft.events.filter((entry) => entry !== type),
    });

  return (
    <Card
      title={draft.id ? `Edit ${draft.name}` : 'New Discord webhook'}
      icon="plug"
      tone="info"
      footer={
        <>
          <Button variant="primary" busy={busy} onClick={onSave}>
            {draft.id ? 'Save' : 'Add'}
          </Button>
          <Button variant="quiet" onClick={onCancel}>
            Cancel
          </Button>
          <span className="spacer" />
          {draft.events.length === 0 ? (
            <span className="quiet">Nothing selected — this destination would never post.</span>
          ) : (
            <span className="quiet">{draft.events.length} events selected</span>
          )}
        </>
      }
    >
      <div className="fields">
        <Field label="Name" hint="What this destination is called in the console.">
          <input
            type="text"
            value={draft.name}
            onChange={(event) => onChange({ ...draft, name: event.target.value })}
          />
        </Field>
        <Field
          label="Webhook address"
          hint={
            draft.id
              ? 'Leave blank to keep the address already saved — it is a credential and is never sent back to this page.'
              : 'Discord → channel settings → Integrations → Webhooks → New Webhook → Copy Webhook URL.'
          }
        >
          <input
            type="url"
            value={draft.url}
            placeholder={draft.id ? 'unchanged' : 'https://discord.com/api/webhooks/…'}
            onChange={(event) => onChange({ ...draft, url: event.target.value })}
          />
        </Field>
      </div>

      <Toggle
        label="Enabled"
        hint="Off keeps the configuration and stops the posts."
        checked={draft.enabled}
        onChange={(next) => onChange({ ...draft, enabled: next })}
      />

      {groups.map((group) => (
        <div key={group}>
          <div className="card-head" style={undefined}>
            <div className="card-head-text">
              <h2>{group}</h2>
            </div>
          </div>
          {catalogue
            .filter((entry) => entry.group === group)
            .map((entry) => (
              <Toggle
                key={entry.type}
                label={entry.label}
                hint={entry.description}
                checked={draft.events.includes(entry.type)}
                onChange={(on) => toggleEvent(entry.type, on)}
              />
            ))}
        </div>
      ))}
    </Card>
  );
}
