import { useEffect, useRef, useState } from 'react';
import { Plus, Loader2, Server, Check, Activity, Pencil, Trash2 } from 'lucide-react';
import type { HeadscaleServer, HeadscaleHealth } from './shared';
import { MIN_HEADSCALE_VERSION } from './shared';
import { useHeadscaleServers, useHeadscaleHealth, headscaleErrorMessage } from './useHeadscaleServers';
import { Card, SectionHeader, Button, Dot, Badge, ErrorNote } from './Cards';
import { ServerForm } from './ServerForm';

// The servers section — register Headscale servers and switch between them. Exactly one is active at a
// time (a DB invariant, not a UI convention), and every other section in this workspace reads it.
//
// EVERY server is probed when this section opens, in parallel, and again for any server registered while
// it is open. A probe costs two upstream round trips (an unauthenticated /version plus an authenticated
// call to prove the stored key still works) — cheap enough at this scale, and the alternative was worse:
// a grey "not checked" dot is the one thing this list must never show, because the reason to look at it
// is to find out which servers are up. A dot that says nothing makes the whole page say nothing.

function timeAgo(iso: string): string {
  const seconds = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
  if (!Number.isFinite(seconds)) return 'unknown';
  if (seconds < 60) return 'just now';
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.round(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  return `${Math.round(hours / 24)}d ago`;
}

type ServerRowProps = {
  server: HeadscaleServer;
  health: HeadscaleHealth | undefined;
  testing: boolean;
  busy: boolean;
  onActivate: () => void;
  onTest: () => void;
  onEdit: () => void;
  onRemove: () => void;
};

const ServerRow = ({ server, health, testing, busy, onActivate, onTest, onEdit, onRemove }: ServerRowProps) => {
  const [confirming, setConfirming] = useState(false);

  // Amber means "asking"; grey should only ever be the frame before the automatic probe starts.
  const tone = health ? (health.ok ? 'ok' : 'bad') : testing ? 'warn' : 'idle';

  return (
    <Card>
      <div className="flex flex-col gap-3 p-3.5">
        <div className="flex items-start gap-2.5">
          <span className="mt-1.5">
            <Dot tone={tone} />
          </span>
          <div className="min-w-0 flex-1">
            <div className="flex items-center gap-2">
              <span className="truncate text-sm font-medium text-zinc-100">{server.name}</span>
              {server.isActive && <Badge tone="active">Active</Badge>}
            </div>
            <div className="mt-0.5 truncate text-xs text-zinc-500" title={server.url}>
              {server.url}
            </div>
            <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-zinc-600">
              <span>{server.version ? `Headscale ${server.version}` : 'version unknown'}</span>
              {server.lastSeenAt && <span>· reached {timeAgo(server.lastSeenAt)}</span>}
              {health?.ok && <span className="text-emerald-400/80">· responded in {health.ms}ms</span>}
            </div>
          </div>
        </div>

        {health && !health.ok && <ErrorNote>{health.error ?? 'The server did not respond'}</ErrorNote>}
        {health?.ok && health.supported === 'unknown' && (
          <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] leading-snug text-amber-300">
            This server reports its version as “{health.version}”, so Officer cannot confirm it is{' '}
            {MIN_HEADSCALE_VERSION} or newer. Self-built images do this; features may behave unexpectedly on older
            builds.
          </div>
        )}

        <div className="flex flex-wrap items-center gap-2">
          {!server.isActive && (
            <Button variant="primary" onClick={onActivate} disabled={busy}>
              <Check className="h-3.5 w-3.5" />
              Use this server
            </Button>
          )}
          <Button onClick={onTest} disabled={testing}>
            {testing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Activity className="h-3.5 w-3.5" />}
            {testing ? 'Testing…' : 'Test'}
          </Button>
          <Button onClick={onEdit} disabled={busy}>
            <Pencil className="h-3.5 w-3.5" />
            Edit
          </Button>
          {confirming ? (
            <>
              <Button variant="danger" onClick={onRemove} disabled={busy}>
                <Trash2 className="h-3.5 w-3.5" />
                Confirm remove
              </Button>
              <Button onClick={() => setConfirming(false)} disabled={busy}>
                Cancel
              </Button>
            </>
          ) : (
            <Button variant="danger" onClick={() => setConfirming(true)} disabled={busy}>
              <Trash2 className="h-3.5 w-3.5" />
              Remove
            </Button>
          )}
        </div>
      </div>
    </Card>
  );
};

const EmptyState = ({ onRegister }: { onRegister: () => void }) => (
  <div className="flex flex-col items-center gap-4 py-16 text-center">
    <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/5 text-zinc-400">
      <Server className="h-6 w-6" />
    </div>
    <div>
      <div className="text-base font-semibold text-zinc-100">No Headscale servers yet</div>
      <p className="mt-1 text-sm text-zinc-500">
        Register a server with its URL and an API key to manage its nodes, users and pre-auth keys from here. Officer
        supports Headscale {MIN_HEADSCALE_VERSION} and newer.
      </p>
    </div>
    <Button variant="primary" onClick={onRegister}>
      <Plus className="h-3.5 w-3.5" />
      Register a server
    </Button>
  </div>
);

export const ServersView = () => {
  const { servers, isLoading, error, refetch, activate, remove } = useHeadscaleServers();
  const healthProbe = useHeadscaleHealth();

  const [formFor, setFormFor] = useState<'new' | HeadscaleServer | null>(null);
  const [health, setHealth] = useState<Record<number, HeadscaleHealth>>({});
  // Several probes are in flight at once now, so this is a set of ids rather than the one id it used to be.
  const [testingIds, setTestingIds] = useState<readonly number[]>([]);
  const [actionError, setActionError] = useState<string | null>(null);

  const test = async (id: number) => {
    setTestingIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
    setActionError(null);
    try {
      const result = await healthProbe.mutateAsync(id);
      setHealth((prev) => ({ ...prev, [id]: result }));
    } catch (err) {
      // A probe that throws is still an answer about the server: record it as a red dot rather than as a
      // page-level error, which would blame the whole screen for one unreachable box.
      setHealth((prev) => ({ ...prev, [id]: { ok: false, error: headscaleErrorMessage(err), ms: 0 } }));
    } finally {
      setTestingIds((prev) => prev.filter((t) => t !== id));
    }
  };

  // Probe every server once per visit to this section, and any server that appears while it is open. The
  // ref is what makes "once" true: the list identity changes when a probe writes lastSeenAt, and without
  // it each result would trigger the next round forever.
  const probed = useRef(new Set<number>());
  useEffect(() => {
    for (const server of servers) {
      if (probed.current.has(server.id)) continue;
      probed.current.add(server.id);
      void test(server.id);
    }
  }, [servers]);

  const run = async (fn: () => Promise<unknown>) => {
    setActionError(null);
    try {
      await fn();
    } catch (err) {
      setActionError(headscaleErrorMessage(err));
    }
  };

  const busy = activate.isPending || remove.isPending;

  return (
    <div className="h-full overflow-y-auto p-4">
      <div className="mx-auto flex w-full max-w-2xl flex-col gap-3">
        <SectionHeader
          title="Servers"
          subtitle="One server is active at a time; every other section acts on it."
          action={
            !formFor && (
              <Button variant="primary" onClick={() => setFormFor('new')} disabled={isLoading}>
                <Plus className="h-3.5 w-3.5" />
                Register a server
              </Button>
            )
          }
        />

        {/* A failed list fetch is reported here, NOT as a replacement for the whole section. It used to be
            an early return, which unmounted the form mid-registration and threw away everything typed into
            it — leaving a reload as the only way to try again. Nothing on this screen may take the form
            off the page except the owner. */}
        {error && (
          <ErrorNote>
            <div className="flex flex-wrap items-center gap-2">
              <span>
                Could not reach the Headscale sidecar: {headscaleErrorMessage(error)}. If it is not running, start it
                with <code className="font-mono">pm2 start ecosystem.config.cjs --only officer-headscale</code>.
              </span>
              <button
                type="button"
                onClick={() => void refetch()}
                className="ml-auto shrink-0 cursor-pointer rounded-lg border border-red-500/30 px-2 py-1 font-medium transition-colors hover:bg-red-500/20"
              >
                Try again
              </button>
            </div>
          </ErrorNote>
        )}

        {/* Keyed by which server it edits: the form seeds its fields from the prop once, at mount, so
            switching straight from one server's Edit to another's would otherwise keep the first one's
            values — and submit diffs those stale values against the NEW server, writing them to it. */}
        {formFor && (
          <ServerForm
            key={formFor === 'new' ? 'new' : formFor.id}
            server={formFor === 'new' ? null : formFor}
            onClose={() => setFormFor(null)}
          />
        )}

        {actionError && <ErrorNote>{actionError}</ErrorNote>}

        {isLoading && (
          <div className="flex items-center justify-center py-16 text-sm text-zinc-500">
            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
            Loading servers…
          </div>
        )}

        {/* Empty state doubles as the registration prompt — there is nothing else to do here without a
            server. Hidden while the form is open, because it is then the same offer twice. */}
        {!isLoading && !error && servers.length === 0 && !formFor && (
          <EmptyState onRegister={() => setFormFor('new')} />
        )}

        {servers.map((server) => (
          <ServerRow
            key={server.id}
            server={server}
            health={health[server.id]}
            testing={testingIds.includes(server.id)}
            busy={busy}
            onActivate={() => void run(() => activate.mutateAsync(server.id))}
            onTest={() => void test(server.id)}
            onEdit={() => setFormFor(server)}
            onRemove={() => void run(() => remove.mutateAsync(server.id))}
          />
        ))}
      </div>
    </div>
  );
};
