import { useEffect, useRef, useState } from 'react';
import { NavLink } from 'react-router';
import type { LucideIcon } from 'lucide-react';
import { resolveIcon } from 'officerdev';
import type { PluginManifest } from 'hooks/useCapabilities';

export type DockItem = {
  label: string;
  to: string;
  color: string;
  // Either a lucide glyph (rendered white on the coloured tile) or an image asset (e.g. an app favicon).
  icon?: LucideIcon;
  image?: string;
};

type DockProps = {
  items: DockItem[];
  className?: string;
  // The content region the dock lives in. Reveal/hide is measured from ITS bottom edge, which the
  // in-flow music dock shrinks when present — so the dock clears the music dock with no magic offset.
  boundaryRef: React.RefObject<HTMLElement | null>;
};

const ICON_SIZE = 48;
const ICON_GAP = 24;
const DOCK_PADDING = 12;
const MAX_SCALE = 1.5;
const MAX_DISTANCE = 150;
const SHOW_THRESHOLD = 24;
const HIDE_THRESHOLD = 100;

const getScale = (mouseX: number | null, iconCenterX: number) => {
  if (mouseX === null) return 1;
  const distance = Math.abs(mouseX - iconCenterX);
  if (distance > MAX_DISTANCE) return 1;
  return 1 + (MAX_SCALE - 1) * Math.cos((distance / MAX_DISTANCE) * (Math.PI / 2));
};

export const Dock = ({ items, className, boundaryRef }: DockProps) => {
  const [mouseX, setMouseX] = useState<number | null>(null);
  const [visible, setVisible] = useState(false);
  const dockRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const handleMouseMove = (ev: MouseEvent) => {
      // Distance from the bottom of the content region (which the in-flow music dock shrinks when
      // present) — so reveal/hide triggers just above wherever the region ends, above the music dock.
      const bottom = boundaryRef.current?.getBoundingClientRect().bottom ?? window.innerHeight;
      const distFromBottom = bottom - ev.clientY;
      setVisible((prev) => (prev ? distFromBottom <= HIDE_THRESHOLD : distFromBottom <= SHOW_THRESHOLD));
      if (distFromBottom <= HIDE_THRESHOLD) {
        const rect = dockRef.current?.getBoundingClientRect();
        if (rect) setMouseX(ev.clientX - rect.left);
      } else {
        setMouseX(null);
      }
    };

    document.addEventListener('mousemove', handleMouseMove);
    return () => document.removeEventListener('mousemove', handleMouseMove);
  }, [boundaryRef]);

  return (
    <div
      ref={dockRef}
      className={`absolute left-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg transition-transform duration-300 ease-in-out ${className ?? 'flex'}`}
      style={{
        backgroundColor: 'var(--dock-bg)',
        borderColor: 'var(--dock-border)',
        bottom: '16px',
        transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`,
      }}
    >
      {items.map((item, index) => {
        const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
        const scale = getScale(mouseX, iconCenter);

        return (
          <NavLink
            key={item.to}
            to={item.to}
            // Home would otherwise light up on every route: without `end`, NavLink treats "/" as an
            // ancestor of everything. Every other item wants the ancestor match, so that a detail route
            // (/plans/x, /system-monitor/btop) keeps its dock tile lit.
            end={item.to === '/'}
            className="group relative flex flex-col items-center"
            style={{
              transform: `scale(${scale})`,
              transformOrigin: 'bottom center',
              transition: 'transform 150ms ease-out',
            }}
          >
            {({ isActive }) => (
              <>
                <span
                  className="absolute -top-9 px-2 py-1 rounded-md text-white text-xs whitespace-nowrap hidden md:block opacity-0 group-hover:opacity-100 transition-opacity duration-150 pointer-events-none"
                  style={{ backgroundColor: 'var(--dock-tooltip-bg)' }}
                >
                  {item.label}
                </span>
                <div
                  className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
                  style={{
                    background: item.color,
                    boxShadow: isActive ? `0 0 12px ${item.color}40` : 'none',
                  }}
                >
                  {item.image ? (
                    <img src={item.image} alt="" className="h-7 w-7 md:h-8 md:w-8 object-contain" />
                  ) : (
                    item.icon && <item.icon className="h-5 w-5 md:h-6 md:w-6 text-white" />
                  )}
                </div>
                {isActive && (
                  <div className="absolute -bottom-1.5 w-1.5 h-1.5 rounded-full" style={{ background: item.color }} />
                )}
              </>
            )}
          </NavLink>
        );
      })}
    </div>
  );
};

import {
  Home,
  MessageCircle,
  FileText,
  FolderOpen,
  Code,
  LayoutGrid,
  FolderKanban,
  Monitor,
  Mail,
  Globe,
  MonitorSmartphone,
  Workflow,
  Music,
  Activity,
  Radio,
  Network,
  ArrowDownUp,
  Bitcoin,
  Receipt,
  Images,
  CalendarDays,
  Contact,
  Clapperboard,
  GitBranch,
  Store,
} from 'lucide-react';

/**
 * The dock items that belong to the SHELL — present on every install, with no sidecar behind them.
 *
 * Everything else is contributed by an installed sidecar's UI manifest and arrives from
 * `/capabilities` at runtime (see `dockItemsFromPlugins`). The split is the point: a feature that can be
 * installed and uninstalled must not be hardcoded here, or the dock would list things this server does
 * not have and the shell would need editing every time a sidecar is added.
 *
 * These are the baseline — chat, files, the terminal and the app's own screens — plus Gitea, which is in
 * the light profile because it fronts a remote instance and installs nothing locally.
 */
export const CORE_DOCK_ITEMS: DockItem[] = [
  { label: 'Home', to: '/', icon: Home, color: '#f59e0b' },
  { label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' },
  { label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
  { label: 'Gitea', to: '/gitea', icon: GitBranch, color: '#609926' },
  { label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
  { label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
  { label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
  { label: 'Browser', to: '/browser', icon: Globe, color: '#06b6d4' },
  { label: 'Monitor', to: '/system-monitor', icon: Activity, color: '#0ea5e9' },
  { label: 'Activity', to: '/activity', icon: Radio, color: '#f59e0b' },
  { label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' },
  // Core because the tailnet is the perimeter — origin checking was removed on the grounds that the
  // tailnet stands in its place, so administering it cannot be an optional extra. It is `kind: 'admin'`,
  // and DashboardLayout filters every tile through canVisit(), so a member never sees this one.
  { label: 'Headscale', to: '/headscale', icon: Network, color: '#818cf8' },
  // Core by necessity: the store is how every other feature arrives, so it can never be one of the
  // things that disappears when uninstalled.
  { label: 'App store', to: '/app-store', icon: Store, color: '#64748b' },
];

/**
 * Turn the manifests of installed sidecars into dock tiles.
 *
 * `resolveIcon` maps a NAME to a glyph, which is why manifests carry names rather than imports — they
 * have to survive being JSON from a marketplace. An unknown name resolves to a neutral box rather than
 * throwing: a plugin naming an icon this build does not have should look plain, not break the dock.
 */
export function dockItemsFromPlugins(plugins: PluginManifest[]): DockItem[] {
  return plugins.flatMap((plugin) => {
    const tile = (t: { name: string; icon?: string; image?: string; color: string; route: string }): DockItem => ({
      label: t.name,
      to: t.route,
      color: t.color,
      ...(t.image ? { image: t.image } : { icon: resolveIcon(t.icon ?? 'Box') }),
    });

    return [
      tile({ name: plugin.name, icon: plugin.icon, image: plugin.image, color: plugin.color, route: plugin.rootRoute }),
      ...(plugin.extraTiles ?? []).map(tile),
    ];
  });
}

/**
 * What is pinned before anyone has chosen. Deliberately drawn only from CORE_DOCK_ITEMS.
 *
 * This used to pin `/music`, which is now an installable sidecar. `useDock` drops a path with no item
 * behind it, so nothing breaks — the default dock just quietly comes up one tile short on a machine
 * where Music was never installed. Defaults that reference optional features are how an app ends up
 * looking subtly wrong on a fresh install for no stated reason.
 */
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/terminal', '/dashboards', '/chat'];
