// The sidebar "update available" card, shared by both shells (the local/desktop
// single-user shell in @executor-js/app and the multiplayer shell here). One
// `<SidebarUpdateCard />` encapsulates the whole decision, because the right way
// to upgrade depends on how this build was deployed:
//
//   - desktop app (the bridge is present): a native "Restart to update" action
//     wired to electron-updater (see ../hooks/desktop-update);
//   - npm-installed CLI / its local web UI (`VITE_UPGRADE_HINT === "npm"`): the
//     copyable `npm i -g executor@latest` command;
//   - self-host and Cloudflare (`"selfhost"` / `"cloudflare"`): a link to that
//     host's upgrade guide, because the steps (image pull, rebuild, redeploy)
//     vary too much for one correct command;
//   - managed cloud (`"managed"`): nothing, it deploys itself.
//
// The "is a newer version published?" verdict comes from the same resolver as
// the CLI notice (@executor-js/api) so the two can never disagree.
import { useCallback, useEffect, useState } from "react";

import { Effect, Exit } from "effect";
import { compareVersions, resolveUpdateChannel, type UpdateChannel } from "@executor-js/api";

import { Button } from "./button";
import { toast } from "./sonner";
import { copyToClipboard } from "../lib/clipboard";
import { type DesktopUpdate, useDesktopUpdate } from "../hooks/desktop-update";

const EXECUTOR_DIST_TAGS_PATH = "/v1/app/npm/dist-tags";
const DOCS_BASE_URL = "https://executor.sh/docs";

type UpgradeHint = "npm" | "selfhost" | "cloudflare" | "managed";

const appEnv = (
  import.meta as ImportMeta & {
    readonly env?: { readonly VITE_APP_VERSION?: string; readonly VITE_UPGRADE_HINT?: string };
  }
).env;
const APP_VERSION = appEnv?.VITE_APP_VERSION;
const UPGRADE_HINT = appEnv?.VITE_UPGRADE_HINT as UpgradeHint | undefined;

// Per-host upgrade guide. A host without an entry (or no hint at all) falls back
// to the docs root rather than a command, so a missing hint can never show a
// wrong upgrade step.
const UPGRADE_DOCS_URL: Partial<Record<UpgradeHint, string>> = {
  selfhost: `${DOCS_BASE_URL}/hosted/docker`,
  cloudflare: `${DOCS_BASE_URL}/hosted/cloudflare`,
};

// ── useLatestVersion ────────────────────────────────────────────────────

function useLatestVersion(currentVersion: string | undefined) {
  const channel: UpdateChannel = currentVersion ? resolveUpdateChannel(currentVersion) : "latest";
  const [latestVersion, setLatestVersion] = useState<string | null>(null);

  useEffect(() => {
    if (!currentVersion) return;
    let cancelled = false;
    void Effect.runPromiseExit(
      Effect.tryPromise({
        try: async () => {
          const res = await fetch(EXECUTOR_DIST_TAGS_PATH);
          if (!res.ok) return null;
          return (await res.json()) as Partial<Record<UpdateChannel, string>>;
        },
        catch: (cause) => cause,
      }),
    ).then((exit) => {
      if (!cancelled && Exit.isSuccess(exit)) {
        setLatestVersion(exit.value?.[channel] ?? null);
      }
    });
    return () => {
      cancelled = true;
    };
  }, [channel, currentVersion]);

  const updateAvailable =
    currentVersion !== undefined &&
    latestVersion !== null &&
    compareVersions(currentVersion, latestVersion) === -1;

  return { latestVersion, updateAvailable, channel };
}

// ── Card chrome ──────────────────────────────────────────────────────────

/** The bordered card with the download glyph + title + version.
 *  Each variant supplies its own action as children. */
function UpdateCardShell(props: {
  title?: string;
  version: string | null;
  children?: React.ReactNode;
}) {
  return (
    <div className="mx-2 mb-2 rounded-xl border border-primary/25 bg-primary/[0.06] p-3">
      <div className="flex items-center gap-2">
        <div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/15">
          <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
            <path
              d="M8 3v7M5 7l3 3 3-3"
              stroke="currentColor"
              strokeWidth="1.4"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
            <path d="M3 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
          </svg>
        </div>
        <div className="min-w-0">
          <p className="text-xs font-semibold text-foreground">
            {props.title ?? "Update available"}
          </p>
          {props.version && <p className="text-sm text-muted-foreground">v{props.version}</p>}
        </div>
      </div>
      {props.children}
    </div>
  );
}

// ── NpmUpdateCard (npm-installed CLI: copyable command) ───────────────────

function NpmUpdateCard(props: { latestVersion: string; channel: UpdateChannel }) {
  const command = `npm i -g executor@${props.channel}`;
  const [copied, setCopied] = useState(false);

  const handleCopy = useCallback(() => {
    void copyToClipboard(command).then((ok) => {
      if (!ok) {
        toast.error("Failed to copy to clipboard");
        return;
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    });
  }, [command]);

  return (
    <UpdateCardShell version={props.latestVersion}>
      <Button
        type="button"
        variant="outline"
        onClick={handleCopy}
        className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-left hover:bg-background/80"
      >
        <code className="truncate font-mono text-xs text-sidebar-foreground">{command}</code>
        <span className="shrink-0 text-muted-foreground transition-colors group-hover:text-foreground">
          {copied ? (
            <svg viewBox="0 0 16 16" fill="none" className="size-3 text-primary">
              <path
                d="M3 8.5l3.5 3.5L13 4"
                stroke="currentColor"
                strokeWidth="1.4"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
          ) : (
            <svg viewBox="0 0 16 16" fill="none" className="size-3">
              <rect
                x="5"
                y="5"
                width="8"
                height="8"
                rx="1.5"
                stroke="currentColor"
                strokeWidth="1.2"
              />
              <path
                d="M3 11V3.5A.5.5 0 013.5 3H11"
                stroke="currentColor"
                strokeWidth="1.2"
                strokeLinecap="round"
              />
            </svg>
          )}
        </span>
      </Button>
    </UpdateCardShell>
  );
}

// ── LinkUpdateCard (self-host / Cloudflare: link to the upgrade guide) ────

function LinkUpdateCard(props: { latestVersion: string; href: string }) {
  return (
    <UpdateCardShell version={props.latestVersion}>
      <a
        href={props.href}
        target="_blank"
        rel="noreferrer"
        className="mt-2.5 flex w-full items-center justify-between gap-2 rounded-lg border border-border/60 bg-background/50 px-2.5 py-1.5 text-xs text-sidebar-foreground transition-colors hover:bg-background/80 hover:text-foreground"
      >
        <span>Upgrade guide</span>
        <svg viewBox="0 0 16 16" fill="none" className="size-3 shrink-0">
          <path
            d="M4 12L12 4M6 4h6v6"
            stroke="currentColor"
            strokeWidth="1.4"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </a>
    </UpdateCardShell>
  );
}

// ── DesktopUpdateCard (desktop app: native restart action) ────────────────

function DesktopUpdateCard(props: { update: DesktopUpdate }) {
  const { status, install } = props.update;
  const version = "version" in status ? status.version : null;

  const action = (() => {
    if (status.state === "downloaded") {
      return (
        <Button
          type="button"
          variant="outline"
          onClick={install}
          className="mt-2.5 w-full rounded-lg border-border/60 bg-background/50 px-2.5 py-1.5 text-xs hover:bg-background/80"
        >
          Restart to update
        </Button>
      );
    }
    if (status.state === "downloading") {
      return (
        <p className="mt-2.5 text-xs text-muted-foreground tabular-nums">
          Downloading… {status.percent}%
        </p>
      );
    }
    if (status.state === "available") {
      return <p className="mt-2.5 text-xs text-muted-foreground">Preparing update…</p>;
    }
    if (status.state === "installing") {
      return <p className="mt-2.5 text-xs text-muted-foreground">Restarting…</p>;
    }
    if (status.state === "error") {
      return <p className="mt-2.5 text-xs text-muted-foreground">{status.message}</p>;
    }
    return null;
  })();

  return (
    <UpdateCardShell
      title={status.state === "error" ? "Update failed" : undefined}
      version={version}
    >
      {action}
    </UpdateCardShell>
  );
}

// ── SidebarUpdateCard (the only export the shells consume) ────────────────

/**
 * The sidebar update card, or null when no update is available (or the host
 * manages its own updates). Reads `VITE_APP_VERSION` / `VITE_UPGRADE_HINT` and
 * the desktop bridge itself, so a shell just drops it in above its footer. Hook
 * order is stable across renders.
 */
export function SidebarUpdateCard(): React.ReactElement | null {
  const desktopUpdate = useDesktopUpdate();
  const { latestVersion, updateAvailable, channel } = useLatestVersion(APP_VERSION);

  // The desktop app updates via electron-updater, so its native card wins
  // wherever the bridge is present (it loads the local app UI, whose hint is
  // "npm" — the bridge overrides it).
  if (desktopUpdate) {
    return desktopUpdate.status.state !== "idle" ? (
      <DesktopUpdateCard update={desktopUpdate} />
    ) : null;
  }

  if (!updateAvailable || !latestVersion) return null;
  // Managed cloud deploys itself: knowing a version exists is not actionable.
  if (UPGRADE_HINT === "managed") return null;
  // The npm-installed CLI is the only deployment a command actually upgrades.
  if (UPGRADE_HINT === "npm") {
    return <NpmUpdateCard latestVersion={latestVersion} channel={channel} />;
  }
  // Self-host / Cloudflare (and any host without an explicit hint): link to the
  // upgrade guide rather than risk a wrong command.
  const href = (UPGRADE_HINT && UPGRADE_DOCS_URL[UPGRADE_HINT]) ?? DOCS_BASE_URL;
  return <LinkUpdateCard latestVersion={latestVersion} href={href} />;
}
