import { useState, useEffect } from 'react';
import {
    Search, Loader2, Check, CircleCheck, CircleAlert, AlertTriangle,
    Download, RefreshCw, Monitor, Globe, ExternalLink, Ban, ScrollText,
} from 'lucide-react';
import { SystemSheet, SheetSection } from '@/components/ui/system-sheet';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { MarkdownContent } from '@/components/ui/MarkdownContent';
import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { formatVersion, isValidVersion } from '@/lib/version';
import { UpdateStatusBadge } from './UpdateStatusBadge';
import { PinnedUpdateBadge } from './PinnedUpdateBadge';
import type { NodeUpdateStatus } from './types';

interface NodeUpdatesSheetProps {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    checkingUpdates: boolean;
    updateStatuses: NodeUpdateStatus[];
    updatingNodeId: number | null;
    /** Mutating affordances (update, update-all, retry, dismiss, recheck) render
     *  only for admins, matching the requireAdmin guard on the fleet routes they
     *  call. Non-admins still see the read-only status table. */
    isAdmin: boolean;
    initialTab?: 'nodes' | 'changelog';
    fetchUpdateStatus: () => Promise<void>;
    triggerNodeUpdate: (nodeId: number) => void;
    triggerNodeReapply: (nodeId: number) => void;
    retryNodeUpdate: (nodeId: number) => void;
    dismissNodeUpdate: (nodeId: number) => void;
    triggerUpdateAll: () => Promise<void>;
}

export function NodeUpdatesSheet({
    open, onOpenChange, checkingUpdates, updateStatuses, updatingNodeId, isAdmin,
    initialTab = 'nodes',
    fetchUpdateStatus, triggerNodeUpdate, triggerNodeReapply, retryNodeUpdate, dismissNodeUpdate, triggerUpdateAll,
}: NodeUpdatesSheetProps) {
    const [search, setSearch] = useState('');
    const [recheckingUpdates, setRecheckingUpdates] = useState(false);
    const [activeTab, setActiveTab] = useState<'nodes' | 'changelog'>(initialTab);
    const [skipLoading, setSkipLoading] = useState<number | null>(null);
    const [releaseNotes, setReleaseNotes] = useState<string | null>(null);
    const [releaseHtmlUrl, setReleaseHtmlUrl] = useState<string | null>(null);
    const [releaseVersion, setReleaseVersion] = useState<string | null>(null);
    const [loadingRelease, setLoadingRelease] = useState(false);
    // The advertised latest version the loaded notes were fetched against, or
    // `undefined` before the first settle (so a null/unknown advertised version
    // still triggers the initial fetch). When it no longer matches the advertised
    // latest, the notes are refetched so the changelog never shows a previous
    // version's notes; settling on any result (including null) records the version
    // so a failed fetch lands on the empty state instead of looping.
    const [loadedForVersion, setLoadedForVersion] = useState<string | null | undefined>(undefined);
    const [hasSeenChangelog, setHasSeenChangelog] = useState(false);

    // The version the Fleet currently advertises as latest (gateway-derived, same
    // as the footer's "Latest version" label). Notes are keyed to this so they
    // stay in sync.
    const advertisedLatest = (updateStatuses.find(s => s.type === 'local') ?? updateStatuses[0])?.latestVersion ?? null;

    useEffect(() => {
        if (open) setActiveTab(initialTab);
    }, [open, initialTab]);

    // Fetch release notes when the sheet opens (changelog shows the current
    // release regardless of update availability), and refetch whenever the
    // advertised latest version changes so reopening after a newer release
    // surfaces its notes, never a previous version's. Pass recheck when the user
    // forced a version recheck so the changelog stays in sync.
    useEffect(() => {
        if (!open || loadingRelease) return;
        if (loadedForVersion === advertisedLatest) return;
        // Drop any previously loaded notes before fetching for a (possibly) newer
        // advertised version. If this fetch mismatches, returns null, errors, or
        // rejects, the panel must fall to the empty state rather than keep showing
        // the prior version's notes; a confirmed match repopulates below. The
        // skeleton (loadingRelease) covers the in-flight gap, so this does not flash.
        setReleaseNotes(null);
        setReleaseHtmlUrl(null);
        setReleaseVersion(null);
        setLoadingRelease(true);
        const recheck = recheckingUpdates ? '?recheck=true' : '';
        apiFetch(`/fleet/update-status/release-notes${recheck}`, { localOnly: true })
            .then(res => res.ok ? res.json() as Promise<{ version: string | null; releaseNotes: string | null; htmlUrl: string | null }> : null)
            .then(data => {
                // Bind strictly to the advertised update: render only notes the
                // endpoint confirms belong to the advertised version. The version
                // lookup and the release-notes lookup use independent caches (and
                // version can fall back to Docker Hub while notes are GitHub-only),
                // so a drifted, null, or failed response keeps the cleared state set
                // above and falls through to the empty state with the online link.
                if (data && data.version !== null && data.version === advertisedLatest) {
                    setReleaseNotes(data.releaseNotes);
                    setReleaseHtmlUrl(data.htmlUrl);
                    setReleaseVersion(data.version);
                }
            })
            .catch((err) => {
                // Informational panel: a failure falls through to the empty state
                // (notes already cleared above) rather than a toast, but leave a
                // breadcrumb so the failure is diagnosable.
                console.warn('[Fleet] Release-notes fetch failed:', err);
            })
            .finally(() => {
                setLoadingRelease(false);
                // Record the advertised version this fetch settled for, so a null
                // or failed result does not loop and a later version change forces
                // a refetch.
                setLoadedForVersion(advertisedLatest);
            });
    }, [open, advertisedLatest, loadedForVersion, loadingRelease, recheckingUpdates]);

    // Clear the changelog dot when user opens that tab.
    useEffect(() => {
        if (open && activeTab === 'changelog') {
            setHasSeenChangelog(true);
        }
    }, [open, activeTab]);

    const handleOpenChange = (next: boolean) => {
        onOpenChange(next);
        if (!next) {
            setSearch('');
            setActiveTab('nodes');
            setHasSeenChangelog(false);
        }
    };

    const handleRecheck = async () => {
        setRecheckingUpdates(true);
        // Force a fresh release-notes fetch with the rechecked version: clearing
        // loadedForVersion makes the effect's version guard miss and refetch.
        setReleaseNotes(null);
        setReleaseHtmlUrl(null);
        setReleaseVersion(null);
        setLoadedForVersion(undefined);
        try {
            const res = await apiFetch('/fleet/update-status?recheck=true', { method: 'DELETE', localOnly: true });
            if (res.ok) {
                // The server throttles the upstream version lookup; `rechecked:false`
                // means a forced refresh ran too recently and the cached value stands.
                const data = await res.json().catch(() => ({}));
                if (data?.rechecked === false) {
                    toast.info('Already checked for the latest version recently.');
                }
            } else {
                // apiFetch only throws on 401/network, so HTTP errors (e.g. a 500
                // from the upstream lookup) land here, not in the catch below.
                console.warn('[Fleet] Recheck returned HTTP', res.status);
                toast.error('Could not recheck for updates. Try again shortly.');
            }
            await fetchUpdateStatus();
        } catch (err) {
            // Recheck is an explicit user click, so a thrown network/auth failure
            // gets a toast, not just a console breadcrumb.
            console.warn('[Fleet] Recheck failed:', err);
            toast.error('Could not recheck for updates. Try again shortly.');
        } finally {
            setRecheckingUpdates(false);
        }
    };

    const handleSkipVersion = async (nodeId: number, version: string | null) => {
        if (!version) return;
        setSkipLoading(nodeId);
        try {
            const res = await apiFetch(`/fleet/nodes/${nodeId}/skip-version`, {
                method: 'POST',
                body: JSON.stringify({ version }),
                localOnly: true,
            });
            if (res.ok || res.status === 204) {
                toast.success('Version skipped.');
                await fetchUpdateStatus();
            } else {
                const err = await res.json().catch(() => ({}));
                toast.error(err?.error || 'Failed to skip version.');
            }
        } catch {
            toast.error('Failed to skip version.');
        } finally {
            setSkipLoading(null);
        }
    };

    const handleUnskipVersion = async (nodeId: number) => {
        setSkipLoading(nodeId);
        try {
            const res = await apiFetch(`/fleet/nodes/${nodeId}/skip-version`, {
                method: 'DELETE',
                localOnly: true,
            });
            if (res.ok || res.status === 204) {
                toast.success('Skip cleared.');
                await fetchUpdateStatus();
            } else {
                toast.error('Failed to clear skip.');
            }
        } catch {
            toast.error('Failed to clear skip.');
        } finally {
            setSkipLoading(null);
        }
    };

    // Dev availability tracks build freshness by digest, not the stable
    // semver compare target, so it is counted separately from stableAvailable.
    // A dev row's updateAvailable is always false (fleet.ts), so upToDate must
    // exclude it too, or a dev-pinned node with a build available would render
    // as "Up to date".
    const stableAvailable = updateStatuses.filter(s => s.updateAvailable && !s.updateStatus).length;
    const devAvailable = updateStatuses.filter(s => s.devBuildUpdateAvailable && !s.updateStatus).length;
    const totalAvailable = stableAvailable + devAvailable;
    const upToDate = updateStatuses.filter(s => !s.updateAvailable && !s.devBuildUpdateAvailable && (!s.updateStatus || s.updateStatus === 'completed')).length;
    const updating = updateStatuses.filter(s => s.updateStatus === 'updating').length;
    const failed = updateStatuses.filter(s => s.updateStatus === 'failed' || s.updateStatus === 'timeout').length;
    const updatableRemoteCount = updateStatuses.filter(s => s.updateAvailable && !s.updateStatus && s.type === 'remote').length;
    const q = search.toLowerCase();
    const filtered = q
        ? updateStatuses.filter(s => s.name.toLowerCase().includes(q) || s.type.includes(q))
        : updateStatuses;
    const localEntry = updateStatuses.find(s => s.type === 'local') ?? updateStatuses[0];
    const gatewayLabel = formatVersion(localEntry?.latestVersion);

    const meta = updateStatuses.length === 0
        ? 'No nodes'
        : `${updateStatuses.length} nodes · ${totalAvailable} update${totalAvailable === 1 ? '' : 's'} available`;

    const footerContext = updateStatuses.length === 0
        ? undefined
        : (gatewayLabel ? `Latest version ${gatewayLabel}` : `${totalAvailable} update${totalAvailable === 1 ? '' : 's'} available`);

    const secondaryActions = isAdmin && updatableRemoteCount > 0
        ? [{
            label: `Update all (${updatableRemoteCount})`,
            icon: Download,
            onClick: () => { void triggerUpdateAll(); },
        }]
        : undefined;

    // A dev build has no release changelog entry, so only a stable release
    // lights the changelog dot.
    const showChangelogDot = stableAvailable > 0 && !hasSeenChangelog;

    const showSkip = (s: NodeUpdateStatus) =>
        s.updateAvailable && !s.updateStatus && isAdmin && isValidVersion(s.version) && isValidVersion(s.latestVersion);

    const tabs: Array<{ id: string; label: string; count?: number; dot?: boolean }> = [
        { id: 'nodes', label: 'Nodes' },
        { id: 'changelog', label: 'Changelog', dot: showChangelogDot },
    ];

    const senchoChangelogLink = (
        <a
            href="https://sencho.io/changelog"
            target="_blank"
            rel="noopener noreferrer"
            className="inline-flex items-center gap-1 text-xs text-brand hover:underline"
        >
            View on Sencho <ExternalLink className="w-3 h-3" strokeWidth={1.5} />
        </a>
    );

    return (
        <SystemSheet
            open={open}
            onOpenChange={handleOpenChange}
            crumb={['Fleet', 'Updates']}
            name="Node updates"
            meta={meta}
            primaryAction={isAdmin ? {
                label: 'Recheck',
                icon: recheckingUpdates ? Loader2 : RefreshCw,
                onClick: () => { void handleRecheck(); },
                disabled: recheckingUpdates || checkingUpdates,
            } : undefined}
            secondaryActions={secondaryActions}
            footerContext={footerContext}
            tabs={tabs}
            activeTab={activeTab}
            onTabChange={(id) => setActiveTab(id as 'nodes' | 'changelog')}
            size="lg"
        >
            {checkingUpdates ? (
                <div className="flex items-center justify-center py-12 text-muted-foreground text-sm gap-2">
                    <Loader2 className="w-4 h-4 animate-spin" />
                    Checking for updates...
                </div>
            ) : updateStatuses.length === 0 ? (
                <div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
                    No nodes found.
                </div>
            ) : activeTab === 'changelog' ? (
                <div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-5">
                    {loadingRelease ? (
                        <div className="space-y-3">
                            <Skeleton className="h-5 w-2/5" />
                            <Skeleton className="h-3 w-1/4" />
                            <div className="space-y-2 pt-2">
                                <Skeleton className="h-3 w-full" />
                                <Skeleton className="h-3 w-11/12" />
                                <Skeleton className="h-3 w-4/5" />
                            </div>
                            <div className="space-y-2 pt-3">
                                <Skeleton className="h-3 w-1/4" />
                                <Skeleton className="h-3 w-full" />
                                <Skeleton className="h-3 w-3/4" />
                            </div>
                        </div>
                    ) : releaseNotes ? (
                        <div className="space-y-4">
                            {releaseVersion && (
                                <div className="text-[10px] font-mono uppercase tracking-[0.18em] text-stat-subtitle">
                                    Release {formatVersion(releaseVersion)}
                                </div>
                            )}
                            <MarkdownContent>{releaseNotes}</MarkdownContent>
                            <div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-card-border/40 pt-3">
                                {releaseHtmlUrl && (
                                    <a
                                        href={releaseHtmlUrl}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="inline-flex items-center gap-1 text-xs text-brand hover:underline"
                                    >
                                        View on GitHub <ExternalLink className="w-3 h-3" strokeWidth={1.5} />
                                    </a>
                                )}
                                {senchoChangelogLink}
                            </div>
                        </div>
                    ) : (
                        <div className="flex flex-1 flex-col items-center justify-center gap-3 py-12 text-center">
                            <ScrollText className="w-8 h-8 text-muted-foreground/40" strokeWidth={1.25} />
                            <div className="space-y-1">
                                <p className="text-sm text-stat-value">No release notes to show</p>
                                <p className="text-xs text-muted-foreground">Read the full changelog online.</p>
                            </div>
                            {senchoChangelogLink}
                        </div>
                    )}
                </div>
            ) : (
                <>
                    <SheetSection title="Summary">
                        <div className="grid grid-cols-4 gap-x-4 divide-x divide-card-border/40 text-center">
                            <div className="px-2">
                                <div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{upToDate}</div>
                                <div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
                                    <CircleCheck className="w-3 h-3 text-success" strokeWidth={1.5} /> Up to date
                                </div>
                            </div>
                            <div className="px-2">
                                <div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{totalAvailable}</div>
                                <div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
                                    <CircleAlert className="w-3 h-3 text-warning" strokeWidth={1.5} /> Available
                                </div>
                            </div>
                            <div className="px-2">
                                <div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{updating}</div>
                                <div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
                                    <Loader2 className="w-3 h-3 text-brand" strokeWidth={1.5} /> Updating
                                </div>
                            </div>
                            <div className="px-2">
                                <div className="text-lg font-medium tabular-nums tracking-tight text-stat-value">{failed}</div>
                                <div className="text-[10px] text-stat-subtitle flex items-center justify-center gap-1">
                                    <AlertTriangle className="w-3 h-3 text-destructive/70" strokeWidth={1.5} /> Failed
                                </div>
                            </div>
                        </div>
                    </SheetSection>

                    <SheetSection title={`Nodes · ${updateStatuses.length}`}>
                        <div className="relative mb-3">
                            <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
                            <Input
                                placeholder="Filter nodes..."
                                value={search}
                                onChange={e => setSearch(e.target.value)}
                                className="h-8 pl-8 text-xs"
                            />
                        </div>

                        <div className="grid grid-cols-[1fr_80px_80px_100px_160px] gap-2 px-3 pb-1 text-[10px] leading-3 font-mono text-stat-subtitle uppercase tracking-[0.18em]">
                            <span>Node</span>
                            <span>Type</span>
                            <span>Current</span>
                            <span>Latest</span>
                            <span className="text-right">Status</span>
                        </div>

                        <div className="divide-y divide-card-border/40">
                            {filtered.map(s => (
                                <div key={s.nodeId} className="grid grid-cols-[1fr_80px_80px_100px_160px] gap-2 items-center px-3 py-2">
                                    <div className="flex items-center gap-2.5 min-w-0">
                                        <div className={`flex items-center justify-center w-6 h-6 rounded-md shrink-0 ${s.updateAvailable && !s.updateStatus ? 'bg-warning/10' : 'bg-muted'}`}>
                                            {s.type === 'local'
                                                ? <Monitor className={`w-3 h-3 ${s.updateAvailable && !s.updateStatus ? 'text-warning' : 'text-muted-foreground'}`} strokeWidth={1.5} />
                                                : <Globe className={`w-3 h-3 ${s.updateAvailable && !s.updateStatus ? 'text-warning' : 'text-muted-foreground'}`} strokeWidth={1.5} />
                                            }
                                        </div>
                                        <span className="text-sm font-medium truncate">{s.name}</span>
                                    </div>
                                    <Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 w-fit">
                                        {s.type}
                                    </Badge>
                                    <span className="text-xs font-mono tabular-nums text-muted-foreground">
                                        {formatVersion(s.version) ?? <span className="text-muted-foreground/50 italic text-[10px]">unknown</span>}
                                    </span>
                                    <span className="text-xs font-mono tabular-nums">
                                        {s.isDevImage
                                            ? <span className="text-muted-foreground/70 italic text-[10px]">Integration build</span>
                                            : formatVersion(s.latestVersion) ?? <span className="text-muted-foreground/50 italic text-[10px]">unknown</span>}
                                    </span>
                                    <div className="flex justify-end items-center gap-1">
                                        {s.updateStatus && (
                                            <UpdateStatusBadge
                                                status={s.updateStatus}
                                                error={s.error}
                                                operationKind={s.operationKind}
                                                onRetry={isAdmin ? () => (
                                                    s.operationKind === 'reapply_configuration'
                                                        ? triggerNodeReapply(s.nodeId)
                                                        : retryNodeUpdate(s.nodeId)
                                                ) : undefined}
                                                onDismiss={isAdmin ? () => dismissNodeUpdate(s.nodeId) : undefined}
                                            />
                                        )}
                                        {!s.updateStatus && !s.updateAvailable && !s.devBuildUpdateAvailable && !s.skipActive && (
                                            <Badge className="text-[10px] px-1.5 py-0 h-5 shrink-0 whitespace-nowrap bg-success-muted text-success border-success/30">
                                                <Check className="w-2.5 h-2.5 mr-0.5" /> Up to date
                                            </Badge>
                                        )}
                                        {s.skipActive && (
                                            <Badge className="text-[10px] px-1.5 py-0 h-5 bg-muted text-muted-foreground border-card-border/40">
                                                <Ban className="w-2.5 h-2.5 mr-0.5" /> Skipped {formatVersion(s.skippedVersion)}
                                            </Badge>
                                        )}
                                        {s.skipActive && isAdmin && (
                                            <Button
                                                variant="ghost"
                                                size="sm"
                                                className="h-6 text-[10px] px-1.5 text-muted-foreground hover:text-stat-value"
                                                onClick={() => { void handleUnskipVersion(s.nodeId); }}
                                                disabled={skipLoading === s.nodeId}
                                            >
                                                Unskip
                                            </Button>
                                        )}
                                        {(s.updateBlocked && s.imageChannel !== 'hardened') && s.updateAvailable && !s.updateStatus && !s.skipActive && (
                                            <PinnedUpdateBadge
                                                reason={s.updateBlockedReason}
                                                className="text-[10px] px-1.5 py-0 h-5 bg-muted text-muted-foreground border-card-border/40"
                                            />
                                        )}
                                        {(s.updateAvailable || s.devBuildUpdateAvailable) && !s.updateStatus && !s.skipActive && !(s.updateBlocked && s.imageChannel !== 'hardened') && isAdmin && (
                                            <Button
                                                variant="outline"
                                                size="sm"
                                                className="h-6 text-[11px] px-2.5"
                                                onClick={() => triggerNodeUpdate(s.nodeId)}
                                                disabled={updatingNodeId === s.nodeId}
                                            >
                                                {updatingNodeId === s.nodeId ? (
                                                    <><Loader2 className="w-3 h-3 mr-1 animate-spin" />Updating</>
                                                ) : (
                                                    <><Download className="w-3 h-3 mr-1" strokeWidth={1.5} />Update</>
                                                )}
                                            </Button>
                                        )}
                                        {isAdmin && !s.updateStatus && s.canReapplyCompose && (
                                            <TooltipProvider delayDuration={300}>
                                                <Tooltip>
                                                    <TooltipTrigger asChild>
                                                        <Button
                                                            variant="ghost"
                                                            size="sm"
                                                            className="h-6 w-6 p-0 shrink-0 text-muted-foreground hover:text-stat-value"
                                                            onClick={() => triggerNodeReapply(s.nodeId)}
                                                            disabled={updatingNodeId === s.nodeId}
                                                            aria-label={updatingNodeId === s.nodeId ? 'Reapplying configuration' : 'Reapply configuration'}
                                                        >
                                                            {updatingNodeId === s.nodeId ? (
                                                                <Loader2 className="w-3 h-3 animate-spin" />
                                                            ) : (
                                                                <RefreshCw className="w-3 h-3" strokeWidth={1.5} />
                                                            )}
                                                        </Button>
                                                    </TooltipTrigger>
                                                    <TooltipContent side="bottom">
                                                        {updatingNodeId === s.nodeId ? 'Reapplying…' : 'Reapply configuration'}
                                                    </TooltipContent>
                                                </Tooltip>
                                            </TooltipProvider>
                                        )}
                                        {isAdmin && !s.updateStatus && s.canReapplyCompose === false && (
                                            <span
                                                className="text-[10px] text-muted-foreground/70 max-w-[9rem] text-right leading-tight"
                                                title={s.type === 'local'
                                                    ? 'This node is not Compose-managed, so configuration reapply is unavailable.'
                                                    : 'This node does not advertise Compose self-management, or is unreachable.'}
                                            >
                                                Reapply unavailable
                                            </span>
                                        )}
                                        {showSkip(s) && (
                                            <Button
                                                variant="ghost"
                                                size="sm"
                                                className="h-6 text-[10px] px-1.5 text-muted-foreground hover:text-warning"
                                                onClick={() => { void handleSkipVersion(s.nodeId, s.latestVersion); }}
                                                disabled={skipLoading === s.nodeId}
                                            >
                                                Skip
                                            </Button>
                                        )}
                                        {(s.updateAvailable || s.devBuildUpdateAvailable) && !s.updateStatus && !s.skipActive && !(s.updateBlocked && s.imageChannel !== 'hardened') && !isAdmin && (
                                            <Badge className="text-[10px] px-1.5 py-0 h-5 bg-warning/15 text-warning border-warning/30">
                                                <CircleAlert className="w-2.5 h-2.5 mr-0.5" /> Available
                                            </Badge>
                                        )}
                                    </div>
                                </div>
                            ))}
                            {filtered.length === 0 && (
                                <div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
                                    No nodes match &ldquo;{search}&rdquo;
                                </div>
                            )}
                        </div>
                    </SheetSection>
                </>
            )}
        </SystemSheet>
    );
}
