import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, Shield, AlertTriangle, ShieldAlert, CircleSlash, Clock, Play, CalendarClock, Monitor, Globe } from 'lucide-react';
import { toast } from '@/components/ui/toast-store';
import { apiFetch, fetchForNode } from '@/lib/api';
import { PaidGate } from '@/components/PaidGate';
import { useNodes } from '@/context/NodeContext';
import type { ScheduledTask } from '@/types/scheduling';

type SemverBump = 'none' | 'patch' | 'minor' | 'major' | 'unknown';

interface UpdatePreviewImage {
  service: string;
  image: string;
  current_tag: string;
  next_tag: string | null;
  has_update: boolean;
  semver_bump: SemverBump;
}

type UpdateKind = 'tag' | 'digest' | 'none';

interface UpdatePreview {
  stack_name: string;
  images: UpdatePreviewImage[];
  summary: {
    has_update: boolean;
    primary_image: string | null;
    current_tag: string | null;
    next_tag: string | null;
    semver_bump: SemverBump;
    update_kind: UpdateKind;
    blocked: boolean;
    blocked_reason: string | null;
  };
  rollback_target: string | null;
  changelog: string | null;
}

interface StackCard {
  stack: string;
  nodeId: number;
  preview: UpdatePreview | null;
  previewLoaded: boolean;
  scheduledTask: ScheduledTask | null;
  applying: boolean;
  autoUpdateEnabled: boolean;
}

interface NodeGroup {
  nodeId: number;
  nodeName: string;
  nodeType: 'local' | 'remote';
  cards: StackCard[];
}

interface FleetUpdateResponse {
  [nodeId: string]: Record<string, boolean>;
}

function formatRelative(ts: number | null): string {
  if (ts == null) return '';
  const delta = ts - Date.now();
  if (delta <= 0) return 'due now';
  const mins = Math.round(delta / 60_000);
  if (mins < 60) return `in ${mins}m`;
  const hours = Math.floor(mins / 60);
  const remMins = mins % 60;
  if (hours < 24) return remMins > 0 ? `in ${hours}h ${remMins}m` : `in ${hours}h`;
  const days = Math.floor(hours / 24);
  const remHours = hours % 24;
  return remHours > 0 ? `in ${days}d ${remHours}h` : `in ${days}d`;
}

function formatClock(ts: number | null): string {
  if (ts == null) return '';
  return new Date(ts).toLocaleString(undefined, {
    weekday: 'short',
    hour: '2-digit',
    minute: '2-digit',
  });
}

function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) {
  if (blocked || bump === 'major') {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full border border-destructive/40 bg-destructive/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-destructive">
        <ShieldAlert className="h-3 w-3" strokeWidth={1.5} />
        Blocked · major
      </span>
    );
  }
  if (bump === 'minor') {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full border border-warning/40 bg-warning/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-warning">
        <AlertTriangle className="h-3 w-3" strokeWidth={1.5} />
        Review · minor
      </span>
    );
  }
  if (bump === 'patch') {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full border border-success/40 bg-success/10 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-success">
        <Shield className="h-3 w-3" strokeWidth={1.5} />
        Safe · patch
      </span>
    );
  }
  if (bump === 'unknown') {
    return (
      <span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
        Digest rebuild
      </span>
    );
  }
  return (
    <span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
      None
    </span>
  );
}

function VersionDiff({ current, next }: { current: string | null; next: string | null }) {
  if (!current) return null;
  const changed = next && next !== current;
  return (
    <div className="flex items-baseline gap-2 font-mono text-sm">
      <span className="text-stat-subtitle">{current}</span>
      <span className="text-stat-subtitle/60">→</span>
      <span className={changed ? 'text-brand font-medium' : 'text-stat-subtitle'}>
        {next ?? current}
      </span>
    </div>
  );
}

function StackReadinessCard({
  card,
  onApply,
}: {
  card: StackCard;
  onApply: (stack: string, nodeId: number) => void;
}) {
  const { stack, nodeId, preview, previewLoaded, scheduledTask, applying, autoUpdateEnabled } = card;
  const loading = !previewLoaded;
  const failed = previewLoaded && preview === null;
  const blocked = preview?.summary.blocked ?? false;
  const bump = preview?.summary.semver_bump ?? 'none';
  const updatingImageCount = preview?.images.filter(i => i.has_update).length ?? 0;
  const nextRun = scheduledTask?.next_run_at ?? null;

  return (
    <Card className="flex flex-col gap-4 p-5">
      <div className="flex items-start justify-between gap-3">
        <div className="flex flex-col gap-1 min-w-0">
          <span className="font-mono text-[10px] uppercase tracking-[0.22em] text-stat-subtitle/80">
            Stack
          </span>
          <span className="font-display italic text-2xl leading-tight tracking-tight text-stat-value truncate">
            {stack}
          </span>
        </div>
        <div className="flex items-center gap-2 shrink-0">
          {!autoUpdateEnabled && (
            <span className="inline-flex items-center gap-1.5 rounded-full border border-card-border bg-muted/30 px-2.5 py-0.5 font-mono text-[10px] leading-3 uppercase tracking-[0.18em] text-stat-subtitle">
              <CircleSlash className="h-3 w-3" strokeWidth={1.5} />
              Auto: Off
            </span>
          )}
          {previewLoaded && preview && <RiskBadge bump={bump} blocked={blocked} />}
        </div>
      </div>

      {loading ? (
        <div className="font-mono text-xs text-stat-subtitle/80">Checking registry...</div>
      ) : failed ? (
        <div className="font-mono text-xs text-destructive/80">
          Preview failed. Registry may be unreachable.
        </div>
      ) : (
        (() => {
          const p = preview!;
          const blockedReason = p.summary.blocked_reason;
          return (
            <>
              {p.summary.update_kind === 'digest' ? (
                <div className="flex items-baseline gap-2 font-mono text-sm">
                  <span className="text-stat-subtitle">{p.summary.current_tag}</span>
                  <span className="text-brand text-[10px] leading-3 uppercase tracking-[0.18em]">
                    Rebuild available
                  </span>
                </div>
              ) : (
                <VersionDiff
                  current={p.summary.current_tag}
                  next={p.summary.next_tag}
                />
              )}

              <div className="flex items-center gap-1.5 font-mono text-[11px] text-stat-subtitle/80">
                <span>{p.summary.primary_image ?? '-'}</span>
                {updatingImageCount > 1 && (
                  <span className="text-stat-subtitle/60">
                    · {updatingImageCount} services
                  </span>
                )}
              </div>

              <div className="border-t border-dashed border-card-border pt-3 text-xs text-stat-subtitle/90 leading-relaxed">
                {p.changelog ?? 'No changelog available from the registry yet.'}
              </div>

              {blocked && blockedReason && (
                <div className="rounded border border-destructive/25 bg-destructive/5 px-3 py-2 text-[11px] text-destructive/90">
                  {blockedReason}
                </div>
              )}

              <div className="mt-auto flex items-center justify-between gap-3 pt-1">
                <div className="flex items-center gap-1.5 font-mono text-[11px] text-stat-subtitle">
                  {nextRun ? (
                    <>
                      <CalendarClock className="h-3.5 w-3.5" strokeWidth={1.5} aria-hidden="true" />
                      <span>Scheduled · <span className="text-stat-value">{formatClock(nextRun)}</span></span>
                      <span className="text-stat-subtitle/70">· {formatRelative(nextRun)}</span>
                    </>
                  ) : (
                    <>
                      <Clock className="h-3.5 w-3.5" strokeWidth={1.5} aria-hidden="true" />
                      <span>No schedule</span>
                    </>
                  )}
                </div>
                <Button
                  size="sm"
                  onClick={() => onApply(stack, nodeId)}
                  disabled={blocked || applying || !autoUpdateEnabled}
                  title={
                    !autoUpdateEnabled
                      ? 'Auto-updates are disabled for this stack. Update it from its actions menu.'
                      : (blocked ? (blockedReason ?? undefined) : undefined)
                  }
                  className="gap-1.5"
                >
                  <Play className="h-3.5 w-3.5" strokeWidth={1.5} aria-hidden="true" />
                  {applying ? 'Applying...' : 'Apply now'}
                </Button>
              </div>
            </>
          );
        })()
      )}
    </Card>
  );
}

function ReadinessHero({
  total,
  ready,
  nodeCount,
  refreshing,
  onRefresh,
}: {
  total: number;
  ready: number;
  nodeCount: number;
  refreshing: boolean;
  onRefresh: () => void;
}) {
  const headline = total === 0
    ? 'Everything is up to date'
    : total === 1
      ? '1 update pending'
      : `${total} updates pending`;
  const acrossNodes = nodeCount > 1
    ? ` across ${nodeCount} nodes`
    : nodeCount === 1
      ? ' across 1 node'
      : '';

  return (
    <div className="relative overflow-hidden rounded-lg border border-brand/25 border-t-brand/35 bg-card shadow-card-bevel">
      <div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-brand/[0.10] via-brand/[0.02] to-transparent" />
      <div className="absolute inset-y-0 left-0 w-[3px] bg-brand" />
      <div className="relative grid grid-cols-[1fr_auto] items-center gap-6 py-5 pl-7 pr-6">
        <div className="flex flex-col gap-1 min-w-0">
          <span className="font-mono text-[10px] uppercase tracking-[0.22em] text-brand">
            Fleet readiness
          </span>
          <span className="font-display italic text-3xl leading-tight tracking-tight text-stat-value">
            {headline}
          </span>
          {total > 0 && (
            <span className="font-mono text-[11px] text-stat-subtitle/90">
              {ready} of {total} ready to apply automatically{acrossNodes}
              {total - ready > 0 ? ` · ${total - ready} blocked by major bump` : ''}
            </span>
          )}
        </div>
        <div className="flex items-center gap-3">
          {total > 0 && (
            <div className="text-right">
              <div className="font-mono tabular-nums text-2xl text-stat-value">
                {ready}<span className="text-stat-subtitle/60"> / {total}</span>
              </div>
              <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
                Ready
              </div>
            </div>
          )}
          <Button
            variant="outline"
            size="sm"
            onClick={onRefresh}
            disabled={refreshing}
            aria-label="Recheck registries"
            className="gap-2"
          >
            <RefreshCw
              className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
              strokeWidth={1.5}
              aria-hidden="true"
            />
            Recheck
          </Button>
        </div>
      </div>
    </div>
  );
}

function NodeGroupSection({
  group,
  onApply,
}: {
  group: NodeGroup;
  onApply: (stack: string, nodeId: number) => void;
}) {
  const TypeIcon = group.nodeType === 'local' ? Monitor : Globe;
  const stackCount = group.cards.length;
  return (
    <section className="flex flex-col gap-4">
      <div className="flex items-baseline gap-3 border-b border-card-border/60 pb-2">
        <TypeIcon className="h-4 w-4 text-stat-subtitle self-center" strokeWidth={1.5} aria-hidden="true" />
        <span className="font-display italic text-xl leading-tight tracking-tight text-stat-value truncate">
          {group.nodeName}
        </span>
        <Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 shrink-0 self-center">
          {group.nodeType}
        </Badge>
        <span className="font-mono text-[11px] text-stat-subtitle/80">
          {stackCount} {stackCount === 1 ? 'stack' : 'stacks'}
        </span>
      </div>
      <div className="grid gap-4 grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3">
        {group.cards.map(card => (
          <StackReadinessCard key={`${card.nodeId}::${card.stack}`} card={card} onApply={onApply} />
        ))}
      </div>
    </section>
  );
}

function AutoUpdateReadinessContent() {
  const { nodes } = useNodes();
  const [groups, setGroups] = useState<NodeGroup[]>([]);
  const [reachableNodeCount, setReachableNodeCount] = useState<number | null>(null);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // Monotonic token guards against stale setGroups from older fetches.
  const loadTokenRef = useRef(0);
  // Holds the latest nodes array so loadReadiness can reference it without
  // re-firing every time NodeContext rebuilds the array on a meta refresh.
  const nodesRef = useRef(nodes);
  nodesRef.current = nodes;

  // Stable signature: only changes when membership or node identity actually
  // changes, not when NodeContext reissues the same logical list.
  const nodesSignature = useMemo(
    () => nodes.map(n => `${n.id}:${n.type}:${n.status}`).sort().join('|'),
    [nodes],
  );

  const localNodeId = useMemo(() => nodes.find(n => n.type === 'local')?.id ?? null, [nodes]);
  const onlineNodeCount = useMemo(() => nodes.filter(n => n.status === 'online').length, [nodes]);

  const loadReadiness = useCallback(async () => {
    const token = ++loadTokenRef.current;
    setLoading(true);
    try {
      const [statusRes, tasksRes] = await Promise.all([
        apiFetch('/image-updates/fleet', { localOnly: true }),
        apiFetch('/scheduled-tasks?action=update', { localOnly: true }),
      ]);
      // Auto-update settings are per-node; fetch lazily after we know which nodes have updates.
      // Collected into a map keyed by nodeId once we know the fleet topology.
      if (token !== loadTokenRef.current) return;

      if (!statusRes.ok) {
        throw new Error('Failed to load fleet update status');
      }
      const fleetStatus = await statusRes.json() as FleetUpdateResponse;
      setReachableNodeCount(Object.keys(fleetStatus).length);

      const tasks: ScheduledTask[] = tasksRes.ok ? await tasksRes.json() : [];
      const taskByNodeStack = new Map<string, ScheduledTask>();
      for (const t of tasks) {
        if (t.target_type !== 'stack' || !t.target_id) continue;
        // Tasks with node_id=null are local-node-scoped.
        const taskNodeId = t.node_id ?? localNodeId;
        if (taskNodeId == null) continue;
        const key = `${taskNodeId}::${t.target_id}`;
        const existing = taskByNodeStack.get(key);
        if (!existing || (t.next_run_at ?? Infinity) < (existing.next_run_at ?? Infinity)) {
          taskByNodeStack.set(key, t);
        }
      }

      // Fetch auto-update settings for all nodes that have pending updates.
      const nodeIdsWithUpdates = [...new Set(
        Object.keys(fleetStatus).map(Number).filter(id => Object.values(fleetStatus[String(id)]).some(Boolean))
      )];
      const autoUpdateByNode = new Map<number, Record<string, boolean>>();
      await Promise.all(nodeIdsWithUpdates.map(async (nodeId) => {
        try {
          const res = await fetchForNode('/stacks/auto-update-settings', nodeId);
          if (res.ok) autoUpdateByNode.set(nodeId, await res.json() as Record<string, boolean>);
        } catch {
          // If the fetch fails, default all stacks on that node to enabled.
        }
      }));

      const flatPairs: { nodeId: number; stack: string }[] = [];
      const initialGroups: NodeGroup[] = [];
      const currentNodes = nodesRef.current;
      for (const [nodeIdStr, stackMap] of Object.entries(fleetStatus)) {
        const nodeId = Number(nodeIdStr);
        const node = currentNodes.find(n => n.id === nodeId);
        if (!node) continue;
        const stacks = Object.entries(stackMap)
          .filter(([, hasUpdate]) => hasUpdate)
          .map(([stack]) => stack)
          .sort();
        if (stacks.length === 0) continue;
        const nodeAutoUpdateSettings = autoUpdateByNode.get(nodeId) ?? {};
        const cards: StackCard[] = stacks.map(stack => {
          flatPairs.push({ nodeId, stack });
          return {
            stack,
            nodeId,
            preview: null,
            previewLoaded: false,
            scheduledTask: taskByNodeStack.get(`${nodeId}::${stack}`) ?? null,
            applying: false,
            autoUpdateEnabled: nodeAutoUpdateSettings[stack] ?? true,
          };
        });
        initialGroups.push({
          nodeId,
          nodeName: node.name,
          nodeType: node.type,
          cards,
        });
      }
      initialGroups.sort((a, b) => {
        if (a.nodeType !== b.nodeType) return a.nodeType === 'local' ? -1 : 1;
        return a.nodeName.localeCompare(b.nodeName);
      });

      if (token !== loadTokenRef.current) return;
      setGroups(initialGroups);

      const previews = await Promise.all(
        flatPairs.map(async ({ nodeId, stack }) => {
          try {
            const res = await fetchForNode(`/stacks/${encodeURIComponent(stack)}/update-preview`, nodeId);
            if (!res.ok) return null;
            return await res.json() as UpdatePreview;
          } catch {
            return null;
          }
        }),
      );
      if (token !== loadTokenRef.current) return;

      const previewByKey = new Map<string, UpdatePreview | null>();
      flatPairs.forEach((pair, idx) => {
        previewByKey.set(`${pair.nodeId}::${pair.stack}`, previews[idx]);
      });

      setGroups(initialGroups.map(g => ({
        ...g,
        cards: g.cards.map(c => ({
          ...c,
          preview: previewByKey.get(`${c.nodeId}::${c.stack}`) ?? null,
          previewLoaded: true,
        })),
      })));
    } catch (err) {
      if (token !== loadTokenRef.current) return;
      toast.error((err as Error)?.message || 'Failed to load readiness');
    } finally {
      if (token === loadTokenRef.current) setLoading(false);
    }
  }, [localNodeId]);

  useEffect(() => {
    if (nodesSignature === '') return;
    loadReadiness();
    return () => {
      // Invalidate any in-flight fetch and cancel pending refresh timers on unmount.
      loadTokenRef.current++;
      if (refreshTimerRef.current) {
        clearTimeout(refreshTimerRef.current);
        refreshTimerRef.current = null;
      }
    };
  }, [loadReadiness, nodesSignature]);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const res = await apiFetch('/image-updates/fleet/refresh', { method: 'POST', localOnly: true });
      if (!res.ok) {
        toast.error('Failed to trigger refresh');
        return;
      }
      const data = await res.json() as { triggered: number[]; rateLimited: number[]; failed: number[] };
      const tCount = data.triggered.length;
      const rCount = data.rateLimited.length;
      const fCount = data.failed.length;
      if (tCount > 0) {
        toast.success(`Rechecking ${tCount} ${tCount === 1 ? 'node' : 'nodes'}...`);
      }
      if (rCount > 0) {
        toast.warning(`${rCount} ${rCount === 1 ? 'node is' : 'nodes are'} rate-limited; try again shortly`);
      }
      if (fCount > 0) {
        toast.error(`${fCount} ${fCount === 1 ? 'node' : 'nodes'} failed to refresh`);
      }
      if (tCount === 0 && rCount === 0 && fCount === 0) {
        toast.info('No reachable nodes to refresh');
        return;
      }
      if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
      refreshTimerRef.current = setTimeout(() => {
        refreshTimerRef.current = null;
        loadReadiness();
      }, 2500);
    } catch (err) {
      toast.error((err as Error)?.message || 'Failed to trigger refresh');
    } finally {
      setRefreshing(false);
    }
  }, [loadReadiness]);

  const handleApply = useCallback(async (stack: string, nodeId: number) => {
    const setCardField = (predicate: (c: StackCard) => boolean, patch: Partial<StackCard>) =>
      setGroups(prev => prev.map(g => ({
        ...g,
        cards: g.cards.map(c => predicate(c) ? { ...c, ...patch } : c),
      })));

    setCardField(c => c.stack === stack && c.nodeId === nodeId, { applying: true });
    const loadingId = toast.loading(`Applying update to ${stack}...`);
    try {
      const res = await fetchForNode(
        `/stacks/${encodeURIComponent(stack)}/update`,
        nodeId,
        { method: 'POST' },
      );
      if (!res.ok) {
        const data = await res.json().catch(() => ({ error: 'Update failed' }));
        throw new Error(data.error ?? 'Update failed');
      }
      toast.success(`${stack} updated successfully`);
      setGroups(prev => prev
        .map(g => g.nodeId === nodeId
          ? { ...g, cards: g.cards.filter(c => c.stack !== stack) }
          : g)
        .filter(g => g.cards.length > 0));
    } catch (err) {
      toast.error((err as Error)?.message || 'Update failed');
      setCardField(c => c.stack === stack && c.nodeId === nodeId, { applying: false });
    } finally {
      toast.dismiss(loadingId);
    }
  }, []);

  const flatCards = useMemo(() => groups.flatMap(g => g.cards), [groups]);
  const { total, ready } = useMemo(() => {
    const t = flatCards.length;
    const r = flatCards.filter(c => c.previewLoaded && c.preview !== null && !c.preview.summary.blocked).length;
    return { total: t, ready: r };
  }, [flatCards]);

  const showPartialBanner = reachableNodeCount != null
    && onlineNodeCount > 0
    && reachableNodeCount < onlineNodeCount;

  return (
    <div className="flex flex-col gap-6 p-6 max-w-[1600px] mx-auto w-full">
      <ReadinessHero
        total={total}
        ready={ready}
        nodeCount={groups.length}
        refreshing={refreshing}
        onRefresh={handleRefresh}
      />

      {showPartialBanner && (
        <div className="font-mono text-[11px] text-stat-subtitle/90 -mt-3 pl-7">
          {reachableNodeCount} of {onlineNodeCount} nodes reachable. Unreachable nodes are not shown.
        </div>
      )}

      {loading && groups.length === 0 ? (
        <div className="flex items-center justify-center py-16 font-mono text-xs text-stat-subtitle">
          Loading readiness...
        </div>
      ) : groups.length === 0 ? (
        <div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-card-border bg-card/40 py-16">
          <Shield className="h-8 w-8 text-success/70" strokeWidth={1.5} aria-hidden="true" />
          <div className="font-display italic text-xl text-stat-value">All stacks on current builds</div>
          <div className="font-mono text-[11px] text-stat-subtitle">
            Sencho will recheck registries on the scheduler interval.
          </div>
        </div>
      ) : (
        <div className="flex flex-col gap-8">
          {groups.map(group => (
            <NodeGroupSection key={group.nodeId} group={group} onApply={handleApply} />
          ))}
        </div>
      )}
    </div>
  );
}

export default function AutoUpdateReadinessView() {
  return (
    <PaidGate>
      <AutoUpdateReadinessContent />
    </PaidGate>
  );
}
