import { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { api } from '../api/client';
import { useAction, useQuery } from '../lib/hooks';
import { useToast } from '../lib/toast';
import { ago, duration, interval, num, until, when } from '../lib/format';
import {
  countSummary,
  dependencyWarning,
  integrationDot,
  integrationTone,
  runOutcome,
  runTone,
} from '../lib/integrations';
import { Glyph } from '../components/Icon';
import {
  Banner,
  Button,
  Card,
  EmptyRow,
  KeyValue,
  Loading,
  Note,
  PageHead,
  TableWrap,
  Tag,
  Tiles,
  Toggle,
} from '../components/ui';
import { RatingsSettingsCard } from './Ratings';
import { RadarrRequestCard, SonarrRequestCard } from './Webhooks';
import type { IntegrationService, IntegrationServiceResponse, TaskRun } from '../api/types';

/* One integration: its switch, its settings, its jobs and everything it has done.
 *
 * A page about one service belongs to the service rather than to the rail, the stance the
 * user, device and task pages take — so it is a hidden destination addressed by an id in
 * the path, and the overview is the way in.
 *
 * It is deliberately the only place a service can be configured. Before it, MDBList lived
 * on a Movie ratings page, the Sonarr and Radarr switches lived on a page about Discord,
 * their request policies lived beside those, and Tracearr could not be reached at all —
 * so "is this integration set up correctly" was four pages and one impossible question.
 *
 * The run history here is not a log. Logs are the technical events behind a failure and
 * they have their own page, which the failure notice links to; this is the operational
 * record of what Memby attempted on this service's behalf and what came of it. They come
 * from the same place — the scheduler's run table, read along the integration axis —
 * which is what keeps them from disagreeing. */

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

export function IntegrationPage() {
  const { integrationId = '' } = useParams();
  const { wrap, show } = useToast();
  const { busy, run } = useAction();
  const [fast, setFast] = useState(false);
  const { data, error, loading, reload } = useQuery<IntegrationServiceResponse>(
    `/admin/api/integrations/services/${encodeURIComponent(integrationId)}?limit=80`,
    { pollMs: fast ? BUSY_POLL_MS : IDLE_POLL_MS, enabled: Boolean(integrationId) },
  );

  const service = data?.service;
  const runs = data?.runs ?? [];
  const running = Boolean(service?.running);
  if (running !== fast) setFast(running);

  const setEnabled = (enabled: boolean) =>
    run('enabled', async () => {
      await wrap(
        () =>
          api.post(
            `/admin/api/integrations/services/${encodeURIComponent(integrationId)}/enabled`,
            { enabled },
          ),
        `${service?.name ?? 'Integration'} ${enabled ? 'enabled' : 'disabled'}.`,
      );
      await reload();
    });

  const test = () =>
    run('test', async () => {
      const result = await wrap(() =>
        api.post<{ reachable: boolean; error?: string; latencyMs: number }>(
          `/admin/api/integrations/services/${encodeURIComponent(integrationId)}/test`,
        ),
      );
      // The probe answers 200 whether or not the service replied, because the *request*
      // succeeded — so the verdict is in the body and reporting it is this page's job.
      if (result) {
        show(
          result.reachable
            ? `${service?.name} answered in ${duration(result.latencyMs)}.`
            : result.error || `${service?.name} did not answer.`,
          result.reachable ? 'ok' : 'bad',
        );
      }
      await reload();
    });

  const runTask = (taskId: string, name: string) =>
    run(taskId, async () => {
      await wrap(
        () => api.post(`/admin/api/tasks/${encodeURIComponent(taskId)}/run`),
        `${name} started.`,
      );
      await reload();
    });

  if (loading || !service) {
    return (
      <>
        <PageHead title="Integration" intro="One external service and everything it has done." />
        <Banner message={error} />
        {loading ? <Loading /> : <Note tone="warn">No such integration.</Note>}
      </>
    );
  }

  const failures = runs.filter((entry) => entry.status === 'failed').length;

  return (
    <>
      <PageHead
        title={service.name}
        intro={service.summary}
        actions={
          <Link className="table-row-link" to="/admin/integrations">
            All integrations
          </Link>
        }
      />

      <Banner message={error} />

      <Tiles
        tiles={[
          {
            label: 'Status',
            value: service.statusLabel,
            small: true,
            icon: 'pulse',
            tone: integrationTone(service.status),
          },
          {
            label: 'Runs recorded',
            value: num(service.runs),
            icon: 'history',
            tone: 'data',
          },
          {
            label: 'Failed runs',
            value: num(service.failures),
            icon: 'alert',
            tone: service.failures > 0 ? 'bad' : undefined,
          },
          {
            label: 'Last worked',
            value: service.lastSuccessAt ? ago(service.lastSuccessAt) : 'never',
            small: true,
            icon: 'check',
            tone: service.lastSuccessAt ? 'ok' : undefined,
          },
        ]}
      />

      <Card
        title="Connection"
        intro="Address and credentials come from this gateway's environment; the switch is stored on the server and applies to the whole household."
        icon="plug"
        tone={integrationTone(service.status) ?? 'info'}
        actions={
          <span className="row tight">
            <span className="dot-state" data-tone={integrationDot(service.status)} />
            <Tag tone={integrationTone(service.status)}>{service.statusLabel}</Tag>
          </span>
        }
        footer={
          service.configured && service.probed ? (
            <Button icon="sync" busy={busy === 'test'} onClick={() => void test()}>
              Test connection
            </Button>
          ) : undefined
        }
      >
        {service.detail ? <Note tone={integrationTone(service.status)}>{service.detail}</Note> : null}

        {!service.configured ? (
          // Stated rather than offered. There is nothing on this page that could fix it:
          // the address and key are environment variables on the container, so a switch
          // here would be one that records a decision nothing ever reads.
          <Note tone="warn">
            This gateway has no address or credential for {service.name}. Set them in the
            deployment's environment and restart the container; there is nothing to switch until
            then.
          </Note>
        ) : (
          <>
            <Toggle
              label={`${service.name} enabled`}
              hint={
                service.enabled
                  ? dependencyWarning(service)
                  : `Memby is not calling ${service.name} or scheduling any of its work.`
              }
              checked={service.enabled}
              disabled={busy === 'enabled'}
              onChange={(enabled) => void setEnabled(enabled)}
            />
            <KeyValue
              rows={[
                { label: 'Address', value: service.address || '—' },
                ...(service.facts ?? []).map((fact) => ({
                  label: fact.label,
                  value: fact.tone ? (
                    <Tag tone={fact.tone === 'warn' ? 'warn' : 'ok'}>{fact.value}</Tag>
                  ) : (
                    fact.value
                  ),
                })),
                {
                  label: 'Last checked',
                  // "Never checked" and "cannot be checked" are different answers, and a
                  // service that is deliberately not probed must not read as one nobody has
                  // got round to looking at.
                  value: !service.probed
                    ? 'Not probed — see below'
                    : service.health
                      ? `${ago(service.health.checkedAt)} · ${duration(service.health.latencyMs)}`
                      : 'not yet',
                },
              ]}
            />
            {!service.probed ? (
              <Note>
                {service.name} is not probed for reachability: its allowance is bought by the day,
                and spending a request of it to draw a status would compete with the televisions
                for the thing being reported on. Its health comes from the run history below.
              </Note>
            ) : null}
          </>
        )}
      </Card>

      {service.powers.length > 0 ? (
        <Card
          title="What depends on this"
          intro="Switching the service off stops all of it. Nothing below fails quietly — it stops being offered."
          icon="journey"
          tone="note"
        >
          {/* The console's list vocabulary rather than a bare <ul>: this is the same
              shape every other enumeration on the console wears, and a page inventing its
              own is how twelve screens stop reading as one. */}
          <div className="list">
            {service.powers.map((power) => (
              <div className="list-item" key={power}>
                <Glyph name="check" tone="ok" />
                <span className="list-body">{power}</span>
              </div>
            ))}
          </div>
        </Card>
      ) : null}

      <IntegrationSettings id={service.id} />

      <Card
        title="Scheduled work"
        intro="The background jobs belonging to this service. Cadence and the per-job switch live on each job's own page; this is where you start one by hand."
        icon="clock"
        tone="info"
      >
        <TableWrap>
          <table>
            <thead>
              <tr>
                <th>Task</th>
                <th>Schedule</th>
                <th>Last run</th>
                <th>Next run</th>
                <th />
              </tr>
            </thead>
            <tbody>
              {service.tasks.length === 0 ? (
                <EmptyRow columns={5}>
                  This service has no scheduled work: Memby calls it when a television asks for
                  something rather than on a timer.
                </EmptyRow>
              ) : (
                service.tasks.map((task) => (
                  <tr key={task.id}>
                    <td>
                      <Link className="table-row-link" to={`/admin/tasks/${encodeURIComponent(task.id)}`}>
                        {task.name}
                      </Link>
                      <span className="table-sub">{task.description}</span>
                    </td>
                    <td className="nowrap muted">{interval(task.intervalSeconds)}</td>
                    <td className="nowrap muted" title={task.lastRun ? when(task.lastRun.startedAt) : undefined}>
                      {task.lastRun ? ago(task.lastRun.startedAt) : 'never'}
                    </td>
                    <td className="nowrap muted">
                      {!task.enabled || !service.enabled ? 'off' : task.running ? 'now' : until(task.nextRun)}
                    </td>
                    <td className="nowrap">
                      <Button
                        size="sm"
                        icon="play"
                        busy={busy === task.id}
                        disabled={task.running}
                        title={`Run ${task.name} now`}
                        onClick={() => void runTask(task.id, task.name)}
                      />
                    </td>
                  </tr>
                ))
              )}
            </tbody>
          </table>
        </TableWrap>
      </Card>

      <Card
        title="Run history"
        intro="What Memby attempted on this service's behalf and what came of it. This is not the application log — a failure here gives the reason, and Logs is where the technical detail behind it lives."
        icon="history"
        tone="note"
        actions={
          failures > 0 ? (
            /* Narrowed to this service by name, which is what makes the link worth
               following: the run history says a request failed and the log says what the
               request was. */
            <Link
              className="table-row-link"
              to={`/admin/logs?q=${encodeURIComponent(service.id)}`}
            >
              Open the logs
            </Link>
          ) : undefined
        }
      >
        <TableWrap>
          <table>
            <thead>
              <tr>
                <th className="nowrap">Started</th>
                <th>Task</th>
                <th>Trigger</th>
                <th>Result</th>
                <th>Outcome</th>
                <th className="num">Checked</th>
                <th className="num">Changed</th>
                <th className="num">Skipped</th>
                <th className="num">Failed</th>
                <th className="num">Took</th>
              </tr>
            </thead>
            <tbody>
              {runs.length === 0 ? (
                <EmptyRow columns={10}>
                  Nothing has run for {service.name} yet.
                </EmptyRow>
              ) : (
                runs.map((entry) => <RunRow key={entry.id} run={entry} service={service} />)
              )}
            </tbody>
          </table>
        </TableWrap>
      </Card>
    </>
  );
}

function RunRow({ run, service }: { run: TaskRun; service: IntegrationService }) {
  const task = service.tasks.find((entry) => entry.id === run.taskId);
  // A figure that is zero is drawn as an em dash rather than as 0: most runs count one or
  // two of the four, and a wall of zeroes reads as a table reporting nothing happened
  // rather than as one reporting what did.
  const figure = (value: number) => (value ? num(value) : '—');
  return (
    <tr>
      <td className="nowrap muted" title={when(run.startedAt)}>
        {ago(run.startedAt)}
      </td>
      <td className="muted">{task?.name ?? run.taskId}</td>
      <td className="muted">{run.trigger}</td>
      <td>
        <Tag tone={runTone(run.status)}>{run.status}</Tag>
      </td>
      <td className="muted">
        {runOutcome(run)}
        {run.error && countSummary(run.counts) ? (
          <span className="table-sub">{countSummary(run.counts)}</span>
        ) : null}
      </td>
      <td className="num muted">{figure(run.counts?.processed ?? 0)}</td>
      <td className="num muted">{figure(run.counts?.changed ?? 0)}</td>
      <td className="num muted">{figure(run.counts?.skipped ?? 0)}</td>
      <td className="num muted">{figure(run.counts?.failed ?? 0)}</td>
      <td className="num muted">{duration(run.durationMs)}</td>
    </tr>
  );
}

/* The settings that belong to one service and nowhere else.
 *
 * A lookup rather than a field on the wire: what a service's settings *are* is markup, and
 * the gateway has no business describing a React component. A service with nothing here
 * renders nothing, which is the Tracearr case — everything about it is environment
 * configuration, and the page says so above. */
function IntegrationSettings({ id }: { id: string }) {
  switch (id) {
    case 'mdblist':
      return <RatingsSettingsCard />;
    case 'sonarr':
      return <SonarrRequestCard />;
    case 'radarr':
      return <RadarrRequestCard />;
    default:
      return null;
  }
}
