import { useCallback } from 'react';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { buildServiceUrl } from '@/lib/serviceUrl';
import {
  createMuteRuleWithToast,
  stackMuteAllDraft,
  stackMuteDeploySuccessDraft,
  stackMuteMonitorDraft,
  labelMuteAllDraft,
  labelMuteExternalDraft,
  labelMuteLowPriorityDraft,
} from '@/lib/muteRules';
import type { StackMenuCtx } from '@/components/sidebar/sidebar-types';
import type { Label as StackLabel, LabelColor } from '../../label-types';
import type { OverlayState } from './useOverlayState';
import type { StackActionsHook } from './useStackActions';
import type { useStackListState } from './useStackListState';
import type { useViewNavigationState } from './useViewNavigationState';
import type { Node } from '@/context/NodeContext';
import { useNodes } from '@/context/NodeContext';
import type { PermissionAction } from '@/context/AuthContext';

type StackListState = ReturnType<typeof useStackListState>;
type NavState = ReturnType<typeof useViewNavigationState>;

interface UseSidebarContextMenuOptions {
  stackListState: StackListState;
  navState: NavState;
  overlayState: OverlayState;
  stackActions: StackActionsHook;
  activeNode: Node | null | undefined;
  isAdmin: boolean;
  can: (action: PermissionAction, resourceType?: string, resourceId?: string, nodeId?: number | null) => boolean;
}

export function useSidebarContextMenu({
  stackListState,
  navState,
  overlayState,
  stackActions,
  activeNode,
  isAdmin,
  can,
}: UseSidebarContextMenuOptions) {
  const { hasCapability } = useNodes();
  const buildMenuCtx = useCallback((file: string): StackMenuCtx => {
    const sName = file.replace(/\.(yml|yaml)$/, '');
    const mainPort = stackListState.stackPorts[file];
    // A partial stack has running containers, so it gets the running-stack
    // lifecycle affordances; the menu's status union stays three-state.
    const rawStatus = stackListState.stackStatuses[file] ?? 'unknown';
    const stackStatus = rawStatus === 'partial' ? 'running' : rawStatus;
    const nodeId = activeNode?.id ?? null;
    const canMuteNotifications = isAdmin && hasCapability('notification-suppression');
    // Fail closed without authoritative status evidence: missing self identity
    // must not read as "ordinary stack", and an unverified port must not offer
    // Open App.
    const ready = stackListState.hydrationReady();
    return {
      stackStatus,
      ready,
      // Identity stays factual: missing evidence must not mislabel an ordinary
      // stack as the Sencho instance. The `ready` gate plus the handler-boundary
      // rechecks are what block actions, not a falsified identity field.
      isSelfStack: stackListState.stackSelfFlags[file] === true,
      // Only offer "Open App" when a browser-reachable URL can actually be built
      // (a remote node with no API host, e.g. a pilot agent, yields none).
      canOpenApp: ready && mainPort !== undefined && buildServiceUrl({ node: activeNode, publicPort: mainPort }) !== null,
      isBusy: stackListState.isStackBusy(file),
      isAdmin,
      canDelete: can('stack:delete', 'stack', sName, nodeId),
      canDeploy: can('stack:deploy', 'stack', sName, nodeId),
      canEditLabels: can('stack:edit', 'stack', sName, nodeId),
      canViewMonitor: can('stack:read', 'stack', sName, nodeId),
      // POST /api/labels (the inline "New label" entry) is guarded by the
      // unscoped requirePermission('stack:edit'); a user with only per-stack
      // scoped edit can toggle existing labels but cannot create new ones.
      canCreateLabels: can('stack:edit'),
      isPinned: stackListState.isPinned(file),
      labels: stackListState.labels,
      assignedLabelIds: (stackListState.stackLabelMap[file] ?? []).map(l => l.id),
      menuVisibility: stackActions.getStackMenuVisibility(file),
      openAlertSheet: () => overlayState.openAlertSheet(file),
      openAutoHeal: () => overlayState.openAutoHeal(file),
      canCheckUpdates: can('stack:deploy', 'stack', sName, nodeId),
      checkUpdates: () => stackActions.checkUpdatesForStack(sName),
      openStackApp: () => stackActions.openStackApp(file),
      deploy: () => stackActions.executeStackActionByFile(file, 'deploy', 'deploy'),
      stop: () => stackActions.executeStackActionByFile(file, 'stop', 'stop'),
      restart: () => stackActions.executeStackActionByFile(file, 'restart', 'restart'),
      update: () => stackActions.executeStackActionByFile(file, 'update', 'update'),
      takeDown: () => stackActions.requestTakeDownStack(sName),
      remove: () => {
        // Readiness recheck at the handler boundary: a menu built while ready
        // must not delete after a node switch or failed refresh.
        if (!stackListState.hydrationReady()) return;
        if (stackListState.stackSelfFlags[file]) {
          overlayState.openSelfStackProtected();
          return;
        }
        overlayState.openDeleteDialog({ name: sName, nodeId: activeNode?.id ?? null });
      },
      pin: () => stackListState.pin(file),
      unpin: () => stackListState.unpin(file),
      toggleLabel: async (labelId: number) => {
        const currentIds = (stackListState.stackLabelMap[file] ?? []).map(l => l.id);
        const assigned = currentIds.includes(labelId);
        const newIds = assigned ? currentIds.filter(id => id !== labelId) : [...currentIds, labelId];
        const loadingId = toast.loading('Updating labels...');
        try {
          const res = await apiFetch(`/stacks/${encodeURIComponent(file)}/labels`, {
            method: 'PUT',
            body: JSON.stringify({ labelIds: newIds }),
          });
          if (!res.ok) {
            const data = await res.json().catch(() => ({}));
            throw new Error((data as { error?: string })?.error || 'Failed to update labels.');
          }
          stackListState.refreshLabels();
        } catch (err: unknown) {
          toast.error((err as Error)?.message || 'Failed to update labels.');
        } finally {
          toast.dismiss(loadingId);
        }
      },
      createAndAssignLabel: async (name: string, color: LabelColor) => {
        const loadingId = toast.loading('Creating label...');
        try {
          const createRes = await apiFetch('/labels', {
            method: 'POST',
            body: JSON.stringify({ name, color }),
          });
          if (!createRes.ok) {
            const data = await createRes.json().catch(() => ({}));
            throw new Error((data as { error?: string })?.error || 'Failed to create label.');
          }
          const created: StackLabel = await createRes.json();
          const currentIds = (stackListState.stackLabelMap[file] ?? []).map(l => l.id);
          const newIds = [...currentIds, created.id];
          const assignRes = await apiFetch(`/stacks/${encodeURIComponent(file)}/labels`, {
            method: 'PUT',
            body: JSON.stringify({ labelIds: newIds }),
          });
          if (!assignRes.ok) {
            const data = await assignRes.json().catch(() => ({}));
            throw new Error((data as { error?: string })?.error || 'Failed to assign label.');
          }
          toast.success(`Label "${created.name}" created.`);
          stackListState.refreshLabels();
        } catch (err: unknown) {
          toast.error((err as Error)?.message || 'Failed to create label.');
          throw err;
        } finally {
          toast.dismiss(loadingId);
        }
      },
      openLabelManager: () => navState.handleOpenSettings('labels'),
      openScheduleTask: () => {
        // Scheduling a lifecycle task needs the same authoritative runtime
        // evidence as running it directly.
        if (!stackListState.hydrationReady()) return;
        navState.setSchedulePrefill({ stackName: sName, nodeId: activeNode?.id ?? null });
        navState.setActiveView('scheduled-ops');
      },
      canMuteNotifications,
      muteStackAll: () => {
        void createMuteRuleWithToast(stackMuteAllDraft(sName, nodeId));
      },
      muteStackDeploySuccess: () => {
        void createMuteRuleWithToast(stackMuteDeploySuccessDraft(sName, nodeId));
      },
      muteStackMonitor: () => {
        void createMuteRuleWithToast(stackMuteMonitorDraft(sName, nodeId));
      },
      openStackMuteRules: () => {
        navState.openMuteRulesWithPrefill(stackMuteAllDraft(sName, nodeId));
      },
      muteLabelAll: (labelId: number, labelName: string) => {
        void createMuteRuleWithToast(labelMuteAllDraft(labelId, labelName, nodeId));
      },
      muteLabelExternal: (labelId: number, labelName: string) => {
        void createMuteRuleWithToast(labelMuteExternalDraft(labelId, labelName, nodeId));
      },
      muteLabelLowPriority: (labelId: number, labelName: string) => {
        void createMuteRuleWithToast(labelMuteLowPriorityDraft(labelId, labelName, nodeId));
      },
      openLabelMuteRules: (labelId: number, labelName: string) => {
        navState.openMuteRulesWithPrefill(labelMuteAllDraft(labelId, labelName, nodeId));
      },
    };
    // Handlers from useStackActions, useOverlayState, useViewNavigationState are
    // useCallback-stabilized at their owner hooks, so listing the menu surface
    // values (status maps, role/tier flags, pin state) is sufficient. Exhaustive
    // deps would force a rebuild on every parent render and defeat the memo.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    stackListState.stackStatuses, stackListState.stackPorts, stackListState.stackSelfFlags, isAdmin, can,
    stackListState.isPinned, stackListState.labels, stackListState.stackLabelMap,
    stackListState.pin, stackListState.unpin, activeNode?.type, activeNode?.api_url, activeNode?.id,
    hasCapability, navState.openMuteRulesWithPrefill,
  ]);

  return buildMenuCtx;
}
