import { useMemo, useState } from 'react';
import {
  HardDrive, Server, Cloud, Plus, Link2, CheckCircle2, AlertCircle, AlertTriangle, X, Database, Pencil, Pause, Play,
  ChevronRight, Copy, Check,
} from 'lucide-react';
import { Card, CardBody, CardHeader, CardTitle } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Field, Input, Select, Switch } from '@/components/ui/Input';
import { Loading, ErrorState, Toast } from '@/components/ui/States';
import { api, type Destination, type DestinationCatalogItem, type DestinationType, type SchemaField } from '@/lib/api';
import { useQuery, useMutation } from '@/hooks/useQuery';
import { formatBytes } from '@/lib/format';
import { cn } from '@/lib/cn';
import { useConfirm } from '@/components/ConfirmProvider';

const iconFor: Record<DestinationType, React.ComponentType<{ className?: string }>> = {
  local: HardDrive, ftp: Server, sftp: Server, google: Cloud, onedrive: Cloud,
  aws: Database, wasabi: Database, s3: Database,
};

const brandFor: Record<DestinationType, string> = {
  local: 'bg-ink-900', ftp: 'bg-orange-500', sftp: 'bg-amber-600',
  google: 'bg-emerald-600', onedrive: 'bg-sky-600',
  aws: 'bg-[#FF9900]', wasabi: 'bg-[#01CD3F]', s3: 'bg-violet-600',
};

const subtitleFor: Record<DestinationType, string> = {
  local: 'Server filesystem',
  ftp: 'Username + password',
  sftp: 'Username + password / key',
  google: 'OAuth 2.0 · 15 GB free',
  onedrive: 'Microsoft Graph · 5 GB free',
  aws: 'Amazon S3 · pay-as-you-go',
  wasabi: 'Hot cloud storage · S3 API',
  s3: 'MinIO, DigitalOcean Spaces, etc.',
};

export function Destinations() {
  const dests   = useQuery(() => api.destinations.list(), []);
  const catalog = useQuery(() => api.destinations.catalog(), []);
  const [openProvider, setOpenProvider] = useState<DestinationType | null>(null);
  /** When set, the form below switches into "edit this destination" mode. */
  const [editing, setEditing] = useState<Destination | null>(null);
  const [toast, setToast] = useState<{ msg: string; tone: 'success' | 'danger' } | null>(null);

  if (dests.isLoading || catalog.isLoading) return <Loading label="Loading destinations…" />;
  if (dests.error)    return <ErrorState error={dests.error}    onRetry={dests.refetch} />;
  if (catalog.error)  return <ErrorState error={catalog.error}  onRetry={catalog.refetch} />;

  const items   = dests.data?.items ?? [];
  const types   = catalog.data?.items ?? [];

  return (
    <div className="space-y-5 max-w-[1400px]">
      <Card>
        <CardBody className="flex items-center justify-between">
          <div>
            <h2 className="text-base font-semibold text-ink-900">Storage destinations</h2>
            <p className="text-xs text-ink-500 mt-0.5">Connect remote storage to keep backups off-site</p>
          </div>
          <Button variant="primary" leftIcon={<Plus className="h-4 w-4" />}
            onClick={() => {
              setOpenProvider('sftp');
              setTimeout(() => document.getElementById('backreload-add-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
            }}>
            Add destination
          </Button>
        </CardBody>
      </Card>

      {/* Connected destinations */}
      {items.length > 0 && (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-stretch">
          {items.map((d) => (
            <DestinationCard key={d.id} dest={d}
              onChanged={(action) => {
                dests.refetch();
                setToast({
                  msg: action === 'test'   ? `Connection to ${d.name} verified`
                     : action === 'remove' ? `${d.name} disconnected`
                                          : 'Destination updated',
                  tone: 'success',
                });
              }}
              onEdit={(toEdit) => {
                setEditing(toEdit);
                setOpenProvider(toEdit.type);
                // Scroll to the form so the user sees the pre-filled fields.
                setTimeout(
                  () => document.getElementById('backreload-add-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
                  50,
                );
              }}
              onError={(m) => setToast({ msg: m, tone: 'danger' })}
            />
          ))}
        </div>
      )}

      {/* Inline add section */}
      <Card id="backreload-add-section">
        <CardHeader>
          <div>
            <CardTitle>{editing ? `Edit ${editing.name}` : 'Add a destination'}</CardTitle>
            <p className="text-xs text-ink-500 mt-0.5">
              {editing
                ? 'Update the connection details below. Leave secret fields blank to keep what\'s already saved.'
                : 'Choose a provider. The connection form appears below.'}
            </p>
          </div>
        </CardHeader>
        <CardBody className="space-y-4">
          {/* Provider picker — hidden in edit mode so the user can't switch type. */}
          {!editing && (
            <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
              {types.map((t) => {
                const Icon = iconFor[t.type] ?? Cloud;
                const active = openProvider === t.type;
                // Server reports whether this destination type can actually
                // be used (e.g. SFTP needs php-ssh2). Greying the tile out
                // up front saves users the round-trip of filling in
                // credentials only to hit the "extension missing" toast.
                const unavailable = t.availability && !t.availability.available;
                return (
                  <button
                    key={t.type}
                    onClick={() => setOpenProvider(active ? null : t.type)}
                    title={unavailable ? `${t.availability!.reason} ${t.availability!.fix}` : undefined}
                    className={cn(
                      'flex items-center gap-3 p-3 rounded-xl border-2 text-left transition-all min-w-0 relative',
                      active ? 'border-brand-500 bg-brand-50'
                             : 'border-ink-200 hover:border-ink-300 hover:bg-ink-50',
                      unavailable && 'opacity-60',
                    )}>
                    <span className={cn('h-9 w-9 grid place-items-center rounded-lg text-white shrink-0', brandFor[t.type])}>
                      <Icon className="h-4 w-4" />
                    </span>
                    <div className="min-w-0 flex-1">
                      <p className="text-sm font-medium text-ink-900 truncate leading-tight">{t.label}</p>
                      <p className="text-[11px] text-ink-500 truncate mt-0.5">
                        {unavailable ? 'PHP extension missing. See details.' : subtitleFor[t.type]}
                      </p>
                    </div>
                    {unavailable && (
                      <span className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full bg-amber-500 ring-2 ring-white" title={t.availability!.reason} />
                    )}
                  </button>
                );
              })}
            </div>
          )}

          {openProvider && (
            <SchemaForm
              type={openProvider}
              schema={types.find((t) => t.type === openProvider)?.schema ?? []}
              label={types.find((t) => t.type === openProvider)?.label ?? openProvider}
              availability={types.find((t) => t.type === openProvider)?.availability}
              setupHelp={types.find((t) => t.type === openProvider)?.setup_help}
              existing={editing}
              onCancel={() => { setOpenProvider(null); setEditing(null); }}
              onCreated={() => {
                setOpenProvider(null);
                setEditing(null);
                setToast({ msg: editing ? 'Destination updated' : 'Destination added', tone: 'success' });
                dests.refetch();
              }}
              onError={(m) => setToast({ msg: m, tone: 'danger' })}
            />
          )}
        </CardBody>
      </Card>

      <div className="flex items-center gap-2 text-xs text-ink-500">
        <AlertCircle className="h-3.5 w-3.5" /> Off-site storage is strongly recommended. Local backups don't protect against server failure.
      </div>

      {toast && <Toast message={toast.msg} tone={toast.tone} onClose={() => setToast(null)} />}
    </div>
  );
}

// ───────────── Destination card ─────────────────────────────────────────

function DestinationCard({ dest, onChanged, onEdit, onError }: {
  dest: Destination;
  onChanged: (action: 'test' | 'remove' | 'update') => void;
  onEdit:    (dest: Destination) => void;
  onError:   (m: string) => void;
}) {
  const Icon = iconFor[dest.type] ?? Cloud;
  const confirm = useConfirm();
  // After a successful Test or Resume, the destination's state went from
  // "maybe broken" to "definitely usable" — auto-fire a sync so any
  // backups sitting on it get pulled into the local list without the user
  // having to click Sync manually. Same logic for Pause (drops cooldown
  // so when they Resume later, sync re-fires) — and we clear both flags
  // so the next sync runs immediately rather than getting throttled by
  // an earlier cooldown.
  const kickSync = () => {
    try {
      if ( typeof window !== 'undefined' ) {
        sessionStorage.removeItem( 'backreload_synced_this_session' );
        localStorage.removeItem( 'backreload_sync_error_cooldown_until' );
      }
      api.backups.sync().catch( () => { /* swallow — explicit Sync surfaces errors */ } );
    } catch { /* never break the destination flow on sync issues */ }
  };
  const test   = useMutation(() => api.destinations.test(dest.id),   { onSuccess: () => { onChanged('test');   kickSync(); }, onError: (e) => onError(e.message) });
  const remove = useMutation(() => api.destinations.remove(dest.id), { onSuccess: () => onChanged('remove'), onError: (e) => onError(e.message) });
  const togglePause = useMutation(
    () => dest.paused ? api.destinations.resume(dest.id) : api.destinations.pause(dest.id),
    { onSuccess: () => { onChanged('update'); if ( dest.paused ) kickSync(); }, onError: (e) => onError(e.message) },
  );
  const isOAuth = dest.type === 'google' || dest.type === 'onedrive';
  const reconnect = async () => {
    try {
      const { auth_url } = await api.oauth.start(dest.type as 'google' | 'onedrive', dest.id);
      openOauthPopup(auth_url, (ok, msg) => {
        if (ok) onChanged('update');
        else    onError(msg || 'OAuth was cancelled or failed');
      });
    } catch (e: any) {
      onError(e?.message ?? String(e));
    }
  };
  // All values are BYTES (see Destination.usage). `total` is the underlying
  // filesystem capacity, NOT a plugin-owned quota. `free` is bytes still
  // available on that filesystem.
  //
  // Bar fill = used / (used + free) — the fraction of available headroom
  // the plugin has consumed. This matches what the user sees in the label
  // ("X used · Y free"): if both numbers are small the bar is empty, if
  // used catches up to free the bar fills up. Previously we showed total
  // disk fullness (whole laptop OS + apps), which produced a half-filled
  // bar even when the plugin had only used a few GB on a mostly-empty disk
  // — confusing for users who don't connect the bar to "total disk used
  // by everything else." The headroom-based ratio is the honest reading
  // of what the labels say.
  //
  // Falls back to legacy `quota` (= total) when reading old API responses.
  const used  = dest.usage?.used ?? 0;
  const free  = dest.usage?.free ?? 0;
  const total = dest.usage?.total ?? dest.usage?.quota ?? 0;
  const headroom = used + free;
  const fillPct  = headroom > 0 ? Math.min(100, ( used / headroom ) * 100) : 0;
  // Only Local destinations report a meaningful filesystem size today. Hide
  // the whole row for remote destinations that just return zeros — better
  // than rendering an empty bar that looks broken.
  const hasUsageData = total > 0 || used > 0;

  return (
    <Card className="flex flex-col h-full">
      <CardBody className="flex flex-col flex-1 gap-4">
        {/* Header row — icon, name, status */}
        <div className="flex items-start gap-3">
          <span className={cn('h-9 w-9 grid place-items-center rounded-lg text-white shrink-0', brandFor[dest.type])}>
            <Icon className="h-4 w-4" />
          </span>
          <div className="flex-1 min-w-0">
            <div className="flex items-center justify-between gap-2">
              <p className="text-sm font-semibold text-ink-900 truncate">{dest.name}</p>
              {dest.paused
                ? <Badge tone="warning" dot>paused</Badge>
                : (
                  <Badge tone={dest.status === 'connected' ? 'success' : dest.status === 'error' ? 'danger' : 'neutral'} dot>
                    {dest.status}
                  </Badge>
                )}
            </div>
            <p className="text-[11px] text-ink-500 mt-0.5 truncate" title={describe(dest)}>{describe(dest)}</p>
            {dest.paused && (
              <p className="text-[11px] text-amber-700 mt-0.5">
                Backups + retention skip this destination until resumed.
              </p>
            )}
          </div>
        </div>

        {/* Usage row. Shows the plugin's own footprint plus the disk's
            remaining headroom. The bar reflects total disk fullness (not
            just the plugin's share) because that's what tells the user
            "you're about to run out of room for backups". */}
        {hasUsageData && (
          <div className="mt-auto">
            <div className="flex items-center justify-between text-[11px] text-ink-500 mb-1.5 gap-2">
              <span>Usage</span>
              <span
                className="text-right truncate"
                title={`This destination holds ${formatBytes(used)} of backup data. The underlying disk has ${formatBytes(free)} free of ${formatBytes(total)} total.`}
              >
                {formatBytes(used)} used
                {total > 0 && <span className="text-ink-400"> · {formatBytes(free)} free</span>}
              </span>
            </div>
            <div className="h-1.5 bg-ink-100 rounded-full overflow-hidden">
              {/* Bar = the plugin's usage as a fraction of "used + free"
                  (matches the label semantics). Tints amber past 80%, red
                  past 95% — when we're running out of headroom regardless
                  of whether other apps caused it. */}
              <div className={cn(
                'h-full rounded-full transition-all',
                fillPct >= 95 ? 'bg-red-500'
                  : fillPct >= 80 ? 'bg-amber-500'
                  : 'bg-brand-500',
              )}
                style={{ width: `${fillPct}%` }} />
            </div>
          </div>
        )}

        {/* Actions row */}
        <div className="flex items-center gap-2 flex-wrap">
          <Button size="sm" variant="secondary" leftIcon={<Link2 className="h-3.5 w-3.5" />}
            disabled={test.isLoading} onClick={() => test.mutate(undefined as any)}>
            {test.isLoading ? 'Testing…' : 'Test'}
          </Button>
          <Button size="sm" variant="ghost"
            leftIcon={dest.paused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
            disabled={togglePause.isLoading}
            onClick={() => togglePause.mutate(undefined as any)}
            title={dest.paused
              ? 'Resume. New backups will use this destination again.'
              : 'Pause. Backups and retention will skip this destination, but it stays configured.'}>
            {togglePause.isLoading
              ? (dest.paused ? 'Resuming…' : 'Pausing…')
              : (dest.paused ? 'Resume' : 'Pause')}
          </Button>
          <Button size="sm" variant="ghost" leftIcon={<Pencil className="h-3.5 w-3.5" />}
            onClick={() => onEdit(dest)}>
            Edit
          </Button>
          {isOAuth && (
            <Button size="sm" variant="ghost" onClick={reconnect}>
              Reconnect
            </Button>
          )}
          <Button size="sm" variant="ghost" className="text-danger ml-auto"
            disabled={remove.isLoading} onClick={async () => {
              const ok = await confirm({
                title: `Disconnect ${dest.name}?`,
                description: 'Backups already uploaded to this destination remain on the remote server.',
                confirmLabel: 'Disconnect',
                tone: 'danger',
              });
              if (ok) remove.mutate(undefined as any);
            }}>
            Disconnect
          </Button>
        </div>
      </CardBody>
    </Card>
  );
}

function describe(d: Destination): string {
  switch (d.type) {
    case 'local':    return (d.config.path as string) ?? '-';
    case 'ftp':
    case 'sftp':     return `${d.config.username ?? ''}@${d.config.host ?? ''}:${d.config.port ?? ''}`;
    case 'google':
    case 'onedrive': return (d.config.folder as string) || 'Connected';
    case 'aws':
    case 'wasabi':
    case 's3':       return `s3://${d.config.bucket ?? ''} (${d.config.region ?? ''})`;
  }
}

// ───────────── Schema-driven form ───────────────────────────────────────

function SchemaForm({
  type, schema, label, availability, setupHelp, existing, onCancel, onCreated, onError,
}: {
  type: DestinationType;
  schema: SchemaField[];
  label: string;
  /** Server-side availability info: greys out the save button + shows a
   *  loud banner when the host is missing the required PHP extension. */
  availability?: { available: boolean; reason: string; fix: string };
  /** Optional step-by-step provider-side setup walkthrough. Server-supplied;
   *  renders as a collapsible help block at the bottom of the form. */
  setupHelp?: DestinationCatalogItem['setup_help'];
  /** When set, the form edits this destination instead of creating a new one. */
  existing?: Destination | null;
  onCancel: () => void;
  onCreated: () => void;
  onError: (m: string) => void;
}) {
  const initial = useMemo(() => {
    const v: Record<string, unknown> = {};
    for (const f of schema) {
      if (existing) {
        // Editing — pre-fill from the destination's current config. Secret
        // fields come back from the API redacted (***), and rendering '***' as
        // the live value would either save it back as the literal secret or
        // confuse the user; instead show secret fields empty + a placeholder.
        const isSecret = !!f.secret;
        const cur = existing.config?.[f.key];
        if (isSecret) {
          v[f.key] = '';
        } else if (cur !== undefined && cur !== null) {
          v[f.key] = cur;
        } else {
          v[f.key] = f.default ?? (f.type === 'bool' ? false : f.type === 'int' ? 0 : '');
        }
      } else {
        v[f.key] = f.default ?? (f.type === 'bool' ? false : f.type === 'int' ? 0 : '');
      }
    }
    return v;
  }, [schema, existing]);

  const [values, setValues] = useState<Record<string, unknown>>(initial);
  const [errors, setErrors] = useState<Record<string, string>>({});
  // Custom display name. Lets the user distinguish multiple destinations of
  // the same type ("FTP — Production", "FTP — Staging", "S3 — Off-site cold
  // storage", …) on the Destinations page, Backups page (the destination
  // badges hover-title), and the Restore picker. Defaults to the provider's
  // generic label on create (e.g. "SFTP") so the user can save without
  // typing anything; pre-fills with the saved name on edit.
  const [customName, setCustomName] = useState<string>( existing?.name ?? label );
  const Icon = iconFor[type] ?? Cloud;

  const isOAuth = type === 'google' || type === 'onedrive';
  const isEdit  = !!existing;

  /** Submit payload: strip empty secret fields on edit so the server keeps the saved value. */
  const submitConfig = () => {
    if (!isEdit) return values;
    const out: Record<string, unknown> = {};
    for (const f of schema) {
      const v = values[f.key];
      if (f.secret && typeof v === 'string' && v === '') continue;
      out[f.key] = v;
    }
    return out;
  };

  // Fallback when the user clears the name field and saves — never let it
  // land empty in the DB, that would render a blank chip everywhere.
  const effectiveName = (customName.trim() || label);

  const create = useMutation(
    () => (isEdit && existing)
      ? api.destinations.update(existing.id, { name: effectiveName, config: submitConfig() })
      : api.destinations.create({ type, name: effectiveName, config: values }),
    {
      onSuccess: async (dest) => {
        // Whenever a destination's config is created or edited, kick off
        // a fresh sync against destinations so any backups sitting at the
        // new path are auto-discovered. Without this, users have to
        // remember to click Sync manually after every path edit — and
        // the auto-sync useEffect on Backups page mount has already
        // run-this-session, so it won't re-fire unless the user opens a
        // brand new tab. Also clear the auto-sync cooldown so the next
        // page navigation doesn't suppress it.
        try {
          if ( typeof window !== 'undefined' ) {
            sessionStorage.removeItem( 'backreload_synced_this_session' );
            localStorage.removeItem( 'backreload_sync_error_cooldown_until' );
          }
          api.backups.sync().catch( () => { /* silent — explicit Sync surfaces errors */ } );
        } catch { /* never let analytics break the destination save */ }

        if (isEdit || !isOAuth) { onCreated(); return; }
        // OAuth (new only) — open popup to provider's auth URL, wait for postMessage from callback.
        try {
          const { auth_url } = await api.oauth.start(type as 'google' | 'onedrive', dest.id);
          openOauthPopup(auth_url, (ok, msg) => {
            if (ok) onCreated();
            else    onError(msg || 'OAuth was cancelled or failed');
          });
        } catch (e: any) {
          onError(e?.message ?? String(e));
        }
      },
      onError:   (e: any) => {
        if (e?.payload?.errors) setErrors(e.payload.errors);
        else onError(e.message ?? String(e));
      },
    },
  );

  return (
    <div className="rounded-xl border border-ink-200 bg-ink-50/40 overflow-hidden">
      <div className="flex items-center justify-between px-4 py-3 border-b border-ink-200 bg-white">
        <div className="flex items-center gap-2.5">
          <span className={cn('h-7 w-7 grid place-items-center rounded-md text-white', brandFor[type])}>
            <Icon className="h-3.5 w-3.5" />
          </span>
          <div>
            <p className="text-sm font-semibold text-ink-900">
              {isEdit ? `Edit ${existing!.name}` : `Configure ${label}`}
            </p>
            <p className="text-[11px] text-ink-500">{subtitleFor[type]}</p>
          </div>
        </div>
        <button onClick={onCancel} title="Cancel" aria-label="Cancel destination setup"
          className="h-7 w-7 grid place-items-center rounded-md text-ink-500 hover:bg-ink-100 hover:text-ink-900">
          <X className="h-4 w-4" />
        </button>
      </div>
      <div className="p-4 space-y-4">
        {/* Unavailability banner — shown when the host can't actually run
            this destination type. Pre-empts the save-time toast that used
            to be the first hint the user got that something was wrong. */}
        {availability && !availability.available && (
          <div className="rounded-lg border-2 border-amber-300 bg-amber-50 p-3.5">
            <div className="flex items-start gap-2.5">
              <AlertTriangle className="h-4 w-4 text-amber-700 shrink-0 mt-0.5" />
              <div className="text-xs text-amber-900 space-y-1">
                <p className="font-semibold">{label} cannot run on this server.</p>
                <p>{availability.reason}</p>
                <p className="text-amber-800"><strong>Fix:</strong> {availability.fix}</p>
              </div>
            </div>
          </div>
        )}

        {/* Custom name. Always at the top of the form so users see it before
            they fill in credentials — and full-width so it stands apart from
            the two-column schema grid below. Optional: blank = label
            fallback. */}
        <div>
          <label className="block text-xs font-medium text-ink-700 mb-1.5">Name</label>
          <input
            type="text"
            value={customName}
            onChange={(e) => setCustomName(e.target.value)}
            placeholder={`e.g. ${label} Production`}
            className="w-full h-9 px-3 text-sm rounded-md border border-ink-200 bg-white focus:border-brand-500 focus:ring-2 focus:ring-brand-100 focus:outline-none"
          />
          <p className="text-[11px] text-ink-500 mt-1">
            Optional. A friendly label so you can tell multiple {label} destinations apart on the Backups and Restore pages. Defaults to "{label}".
          </p>
        </div>

        <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
          {(() => {
            const nonBool = schema
              .map((f, i) => ({ f, i }))
              .filter((x) => x.f.type !== 'bool');
            const lastIsOdd      = nonBool.length % 2 === 1;
            const lastNonBoolIdx = lastIsOdd ? nonBool[nonBool.length - 1].i : -1;

            return schema.map((f, i) => {
              const span2 = f.type === 'bool' || i === lastNonBoolIdx;
              // In edit mode, secret fields show empty (the API redacts them
              // with `***`) so the placeholder explains what blank means.
              const placeholder = isEdit && f.secret ? 'Leave blank to keep current' : undefined;
              return (
                <div key={f.key} className={span2 ? 'sm:col-span-2' : ''}>
                  <FormField field={f} value={values[f.key]} error={errors[f.key]} placeholder={placeholder}
                    onChange={(v) => { setValues({ ...values, [f.key]: v }); if (errors[f.key]) setErrors({ ...errors, [f.key]: '' }); }} />
                </div>
              );
            });
          })()}
        </div>
        {isOAuth && (
          <div className="flex items-start gap-2.5 rounded-lg border border-brand-200 bg-brand-50 p-3">
            <Cloud className="h-4 w-4 text-brand-700 shrink-0 mt-0.5" />
            <p className="text-xs text-brand-900">
              After saving, a {type === 'google' ? 'Google' : 'Microsoft'} sign-in popup will open.
              Authorize BackReload to access your {type === 'google' ? 'Drive' : 'OneDrive'}. The window closes automatically when done.
            </p>
          </div>
        )}

        {/* Provider-side setup walkthrough — only rendered for destinations
            that supply a setup_help payload (Google, OneDrive). Collapsed
            by default so users who already have credentials aren't
            distracted; opens to a numbered step list plus copy-friendly
            callout boxes for the OAuth redirect URI etc. */}
        {setupHelp && <SetupHelpBlock help={setupHelp} />}

        <div className="flex items-center gap-2 pt-2 border-t border-ink-200">
          <Button
            variant="primary"
            leftIcon={isEdit
              ? <CheckCircle2 className="h-4 w-4" />
              : isOAuth
                ? (type === 'google' ? <GoogleMark /> : <MicrosoftMark />)
                : <CheckCircle2 className="h-4 w-4" />}
            disabled={create.isLoading || (availability && !availability.available)}
            title={availability && !availability.available ? availability.fix : undefined}
            onClick={() => create.mutate(undefined as any)}
          >
            {create.isLoading
              ? (isEdit ? 'Updating…' : isOAuth ? 'Opening sign-in…' : 'Saving…')
              : isEdit
                ? 'Update destination'
                : (isOAuth
                  ? (type === 'google' ? 'Sign in with Google' : 'Sign in with Microsoft')
                  : 'Save destination')}
          </Button>
          <Button variant="ghost" onClick={onCancel} className="ml-auto">Cancel</Button>
        </div>
      </div>
    </div>
  );
}

/**
 * Inline "where do I get these credentials?" walkthrough rendered below the
 * config form for OAuth destinations (Google Drive, OneDrive). Content comes
 * from the server's setup_help() static method — we don't compose anything
 * provider-specific in the React layer, which keeps the per-destination doc
 * in PHP alongside the rest of the destination code.
 *
 * Steps may contain a small allowlist of HTML tags (<strong>, <code>, <a>)
 * so they read naturally without forcing the PHP author to escape every
 * piece of inline emphasis. Rendered via dangerouslySetInnerHTML — safe
 * here because the source is server-controlled (never user input).
 *
 * Callouts (the boxed values like the redirect URI) get a copy button.
 */
function SetupHelpBlock({ help }: { help: NonNullable<DestinationCatalogItem['setup_help']> }) {
  const [copiedIdx, setCopiedIdx] = useState<number | null>(null);

  const copy = async (text: string, idx: number) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopiedIdx(idx);
      // Reset the "Copied" badge after a beat so the next click can re-confirm.
      setTimeout(() => setCopiedIdx((cur) => (cur === idx ? null : cur)), 1400);
    } catch {
      // navigator.clipboard requires HTTPS/localhost; on plain HTTP it will throw.
      // Fall back to the old textarea-selection technique so the user isn't stuck.
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.opacity = '0';
      document.body.appendChild(ta);
      ta.select();
      try { document.execCommand('copy'); setCopiedIdx(idx); } catch {}
      document.body.removeChild(ta);
      setTimeout(() => setCopiedIdx((cur) => (cur === idx ? null : cur)), 1400);
    }
  };

  return (
    <details className="group mt-1 rounded-xl border border-ink-200 bg-ink-50/40 open:bg-white">
      <summary className="list-none cursor-pointer px-4 py-3 flex items-center gap-2 select-none">
        <ChevronRight className="h-4 w-4 text-ink-500 transition-transform group-open:rotate-90" />
        <span className="text-sm font-semibold text-ink-800">{help.title}</span>
      </summary>
      <div className="px-4 pb-4 pt-1 space-y-3">
        <p className="text-[11px] uppercase tracking-wider font-medium text-ink-500">
          {help.provider}
        </p>
        <ol className="space-y-2.5 text-xs text-ink-700 leading-relaxed list-decimal pl-4">
          {help.steps.map((html, i) => (
            <li key={i} className="pl-1 [&_a]:text-brand-700 [&_a]:underline [&_a]:underline-offset-2 [&_a:hover]:text-brand-800 [&_code]:font-mono [&_code]:text-[11px] [&_code]:bg-ink-100 [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_strong]:text-ink-900"
              // Safe: server-controlled HTML, never user input. See SetupHelpBlock doc.
              dangerouslySetInnerHTML={{ __html: html }}
            />
          ))}
        </ol>
        {help.callouts && help.callouts.length > 0 && (
          <div className="space-y-2 pt-1">
            {help.callouts.map((c, i) => (
              <div key={i} className="rounded-lg border border-ink-200 bg-white p-3">
                <p className="text-[11px] font-medium text-ink-700">{c.label}</p>
                <div className="mt-1.5 flex items-stretch gap-2">
                  <code
                    className="flex-1 min-w-0 font-mono text-[11px] text-ink-800 bg-ink-50 border border-ink-200 rounded px-2 py-1.5 break-all"
                    title={c.value}
                  >
                    {c.value}
                  </code>
                  <button
                    type="button"
                    onClick={() => copy(c.value, i)}
                    aria-label={`Copy ${c.label}`}
                    title={copiedIdx === i ? 'Copied' : 'Copy to clipboard'}
                    className={cn(
                      'shrink-0 inline-flex items-center gap-1 px-2.5 text-[11px] font-medium rounded border transition-colors',
                      copiedIdx === i
                        ? 'border-emerald-300 bg-emerald-50 text-emerald-800'
                        : 'border-ink-200 bg-white text-ink-700 hover:bg-ink-50 hover:text-ink-900'
                    )}
                  >
                    {copiedIdx === i
                      ? <><Check className="h-3 w-3" /> Copied</>
                      : <><Copy className="h-3 w-3" /> Copy</>}
                  </button>
                </div>
                {c.help && (
                  <p className="text-[11px] text-ink-500 mt-1.5">{c.help}</p>
                )}
              </div>
            ))}
          </div>
        )}
      </div>
    </details>
  );
}

/**
 * Open the OAuth provider in a popup and resolve when the callback posts back.
 * The callback HTML calls `window.opener.postMessage({backreloadOauth, message})`.
 */
function openOauthPopup(authUrl: string, onResult: (ok: boolean, msg?: string) => void) {
  const w = 540, h = 700;
  const left = window.screenX + (window.outerWidth  - w) / 2;
  const top  = window.screenY + (window.outerHeight - h) / 2;
  const popup = window.open(authUrl, 'backreload-oauth', `width=${w},height=${h},left=${left},top=${top}`);
  if (!popup) {
    onResult(false, 'Popup blocked. Allow popups for this site and try again.');
    return;
  }

  const listener = (event: MessageEvent) => {
    if (event.origin !== window.location.origin) return;
    const data = event.data as { backreloadOauth?: boolean; message?: string } | null;
    if (!data || typeof data.backreloadOauth === 'undefined') return;
    window.removeEventListener('message', listener);
    onResult(!!data.backreloadOauth, data.message);
  };
  window.addEventListener('message', listener);

  // If the user just closes the popup without finishing, surface that.
  const closedTimer = window.setInterval(() => {
    if (popup.closed) {
      window.clearInterval(closedTimer);
      window.removeEventListener('message', listener);
      // postMessage might still arrive briefly — give it a moment.
      setTimeout(() => onResult(false, 'Sign-in window was closed before completing.'), 500);
    }
  }, 700);
}

/* Brand marks for the Sign-in buttons. Kept inline so we don't pull a 3rd-party icon set. */
function GoogleMark() {
  return (
    <svg width="16" height="16" viewBox="0 0 18 18" aria-hidden="true">
      <path fill="#fff" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 01-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"/>
      <path fill="#fff" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.83.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.32A8.99 8.99 0 009 18z"/>
      <path fill="#fff" d="M3.97 10.72A5.4 5.4 0 013.68 9c0-.6.1-1.18.29-1.72V4.96H.96A9 9 0 000 9c0 1.45.35 2.82.96 4.04l3.01-2.32z"/>
      <path fill="#fff" d="M9 3.58c1.32 0 2.5.46 3.44 1.35l2.58-2.58A8.99 8.99 0 00.96 4.96l3.01 2.32C4.68 5.16 6.66 3.58 9 3.58z"/>
    </svg>
  );
}
function MicrosoftMark() {
  return (
    <svg width="16" height="16" viewBox="0 0 18 18" aria-hidden="true">
      <rect x="1" y="1"  width="7.5" height="7.5" fill="#fff"/>
      <rect x="9.5" y="1"  width="7.5" height="7.5" fill="#fff"/>
      <rect x="1" y="9.5" width="7.5" height="7.5" fill="#fff"/>
      <rect x="9.5" y="9.5" width="7.5" height="7.5" fill="#fff"/>
    </svg>
  );
}

function FormField({ field, value, error, placeholder, onChange }: {
  field: SchemaField;
  value: unknown;
  error?: string;
  placeholder?: string;
  onChange: (v: unknown) => void;
}) {
  if (field.type === 'bool') {
    return (
      <div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-ink-200 bg-white">
        <div>
          <p className="text-sm text-ink-800">{field.label}</p>
          {field.help && <p className="text-[11px] text-ink-500 mt-0.5">{field.help}</p>}
        </div>
        <Switch checked={!!value} onChange={(v) => onChange(v)} />
      </div>
    );
  }
  if (field.type === 'select') {
    return (
      <Field label={field.label} hint={error || field.help}>
        <Select value={String(value ?? '')} onChange={(e) => onChange(e.target.value)}>
          {(field.options ?? []).map((o) => <option key={o} value={o}>{o}</option>)}
        </Select>
      </Field>
    );
  }
  return (
    <Field label={field.label} hint={error || field.help}>
      <Input
        type={field.type === 'password' ? 'password' : (field.type === 'int' ? 'number' : 'text')}
        value={String(value ?? '')}
        placeholder={placeholder}
        onChange={(e) => onChange(field.type === 'int' ? parseInt(e.target.value || '0', 10) : e.target.value)}
        className={error ? 'border-danger focus:border-danger focus:ring-red-200' : ''}
      />
    </Field>
  );
}
