import React, { useState, useEffect, useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useAuth } from '../App'
import * as pdfjsLib from 'pdfjs-dist'
import { AnnotationMode } from 'pdfjs-dist'
import LineItemHistoryModal from './LineItemHistoryModal'
import CreateDisputeModal from './CreateDisputeModal'
import DisputeDetailModal from './DisputeDetailModal'
import LinkDisputeModal from './LinkDisputeModal'
import PurchaseOrderModal from './PurchaseOrderModal'
import IngredientModal from './IngredientModal'
import { IngredientModalResult, LineItemResult } from '../utils/ingredientHelpers'

// Use local worker file from public directory
pdfjsLib.GlobalWorkerOptions.workerSrc = '/kitchen/pdf.worker.min.mjs'

// Simple Scale icon SVG component
const ScaleIcon = ({ style }: { style?: React.CSSProperties }) => (
  <svg
    viewBox="0 0 24 24"
    fill="currentColor"
    style={{ width: '18px', height: '18px', ...style }}
  >
    <path d="M12 3c-1.27 0-2.4.8-2.82 2H3v2h1.95L2 14c-.47 2 1 4 4 4s4.5-2 4-4L7.05 7H9.18c.35.99 1.17 1.74 2.18 1.94V19H7v2h10v-2h-4.18V8.94c1.01-.2 1.83-.95 2.18-1.94h2.13L14 14c-.47 2 1 4 4 4s4.5-2 4-4l-2.95-7H21V5h-6.18C14.4 3.8 13.27 3 12 3zm0 2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM6 10.45L7.5 14h-3L6 10.45zm12 0L19.5 14h-3L18 10.45z" />
  </svg>
)

// Source badge component - shows where invoice came from
const sourceConfig: Record<string, { label: string; color: string; icon: string }> = {
  upload: { label: 'Upload', color: '#6c757d', icon: '📤' },
  email: { label: 'Email', color: '#3498db', icon: '📧' },
  api: { label: 'API', color: '#9b59b6', icon: '🔌' },
}

const SourceBadge = ({ source, sourceReference }: { source: string; sourceReference?: string | null }) => {
  const { label, color, icon } = sourceConfig[source] || sourceConfig.upload
  return (
    <span
      style={{
        background: color,
        color: 'white',
        padding: '0.4rem 0.6rem',
        borderRadius: '4px',
        fontSize: '0.8rem',
        fontWeight: '600',
        display: 'inline-flex',
        alignItems: 'center',
        gap: '0.25rem',
      }}
      title={sourceReference || undefined}
    >
      {icon} {label}
    </span>
  )
}

interface Invoice {
  id: number
  invoice_number: string | null
  invoice_date: string | null
  total: number | null
  net_total: number | null
  stock_total: number | null
  supplier_id: number | null
  supplier_name: string | null
  supplier_match_type: string | null  // "exact", "fuzzy", or null
  supplier_skip_dext: boolean  // Whether supplier has skip_dext enabled
  vendor_name: string | null  // OCR-extracted vendor name
  status: string
  category: string | null
  ocr_confidence: number | null
  ocr_raw_text: string | null  // OCR text or error message
  image_path: string
  document_type: string | null
  order_number: string | null
  duplicate_status: string | null
  duplicate_of_id: number | null
  // Dext integration
  notes: string | null
  dext_sent_at: string | null
  dext_sent_by_username: string | null
  // Dispute tracking
  dispute_count: number
  has_open_disputes: boolean
  disputes: Array<{
    id: number
    dispute_type: string
    status: string
    title: string
    disputed_amount: number
    opened_at: string
  }>
  disputed_line_item_ids: number[]
  // Source tracking
  source: string
  source_reference: string | null
  // Linked dispute (for credit notes)
  linked_dispute_id: number | null
  // Total pages from OCR (for multi-page invoices)
  total_pages: number | null
}

interface Supplier {
  id: number
  name: string
  aliases?: string[]
}

interface LineItem {
  id: number
  product_code: string | null
  description: string | null
  description_alt: string | null  // Alternative description (Azure content vs value mismatch)
  unit: string | null
  quantity: number | null
  order_quantity: number | null
  unit_price: number | null
  tax_rate: string | null
  tax_amount: number | null
  amount: number | null
  line_number: number
  is_non_stock: boolean
  // Pack size fields
  raw_content: string | null
  pack_quantity: number | null
  unit_size: number | null
  unit_size_type: string | null
  portions_per_unit: number | null  // null = not defined yet
  cost_per_item: number | null
  cost_per_portion: number | null
  // OCR warnings for values that needed correction
  ocr_warnings: string | null
  // Price change tracking
  price_change_status: string | null  // "consistent", "amber", "red", "no_history", "acknowledged"
  price_change_percent: number | null
  previous_price: number | null
  // Future price (for old invoices)
  future_price: number | null
  future_change_percent: number | null
  // Page number from OCR (for multi-page invoices)
  page_number: number | null
  // Ingredient mapping
  ingredient_id?: number | null
  ingredient_name?: string | null
  ingredient_unit?: string | null
}

// Ingredient types for mapping modal
interface IngredientSuggestion {
  id: number
  name: string
  category_name: string | null
  standard_unit: string
  yield_percent: number
  similarity?: number
}


// Scale icon color based on data completeness
const getScaleIconColor = (item: LineItem): string => {
  if (item.ingredient_id) return '#28a745'  // Green - mapped to ingredient
  if (!item.pack_quantity) return '#dc3545'  // Red - no pack data
  if (item.portions_per_unit === null) return '#ffc107'  // Amber - portions not defined
  if (item.cost_per_portion !== null) return '#28a745'  // Green - fully complete
  return '#ffc107'  // Amber - fallback
}

interface DuplicateCompare {
  current_invoice: Invoice
  firm_duplicate: Invoice | null
  possible_duplicates: Invoice[]
  related_documents: Invoice[]
}

interface ProductDefinition {
  id: number
  kitchen_id: number
  supplier_id: number | null
  product_code: string | null
  description_pattern: string | null
  pack_quantity: number | null
  unit_size: number | null
  unit_size_type: string | null
  portions_per_unit: number | null
  portion_description: string | null
  saved_by_user_id: number | null
  saved_by_username: string | null
  source_invoice_id: number | null
  source_invoice_number: string | null
  updated_at: string | null
}

interface Settings {
  high_quantity_threshold: number
  dext_manual_send_enabled: boolean
  pdf_preview_show_annotations: boolean
  // LLM FEATURE — see LLM-MANIFEST.md for removal instructions
  llm_enabled?: boolean
  anthropic_api_key_set?: boolean
}

// LLM FEATURE — see LLM-MANIFEST.md for removal instructions
interface AiAssistSuggestions {
  supplier_match?: { id: number; name: string; confidence: number; reason: string } | null
  corrected_date?: string | null
  line_item_corrections?: Array<{ idx: number; field: string; current: number | string | null; suggested: number | string; reason: string }>
  description_recommendations?: Array<{ idx: number; recommendation: string; reason: string }>
  subtotal_flags?: number[]
  total_mismatch_analysis?: string | null
  vat_treatment?: string | null
  pack_size_suggestions?: Array<{ idx: number; pack_quantity: number; unit_size: number; unit_size_type: string }>
}

interface AiReconciliationMatch {
  idx: number
  ingredient_id: number
  ingredient_name: string
  confidence: number
  reason: string
}

interface SearchResultItem {
  description: string
  unit_price: number | null
  unit: string | null
  pack_info: string | null
  last_invoice_date: string | null
  invoice_id: number
  similarity: number
}

interface SupplierSearchGroup {
  supplier_id: number | null
  supplier_name: string
  items: SearchResultItem[]
}

interface SearchResponse {
  query: string
  extracted_keywords: string
  results: SupplierSearchGroup[]
  total_matches: number
}

const TOLERANCE = 0.02; // 2p tolerance for rounding

// Helper to get first line of description (before newline)
const getFirstLineOfDescription = (description: string | null): string => {
  if (!description) return '';
  return description.split('\n')[0].trim();
};

// Date warning levels for unconfirmed invoices
type DateWarning = 'none' | 'amber' | 'red' | 'future';
function getDateWarning(dateStr: string | null, status: string): DateWarning {
  // No date = always red (regardless of status)
  if (!dateStr) return 'red';

  // Only warn for unconfirmed invoices (confirmed invoices can have any date)
  if (status === 'CONFIRMED') return 'none';

  const invoiceDate = new Date(dateStr);
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  invoiceDate.setHours(0, 0, 0, 0);

  // Future date = almost certainly a day/month swap error — highest priority
  if (invoiceDate > today) return 'future';

  const diffMs = today.getTime() - invoiceDate.getTime();
  const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

  // More than 7 days old: escalate based on how far back and whether it crosses month/year
  if (diffDays > 7) {
    const sameMonth = invoiceDate.getMonth() === today.getMonth();
    const sameYear = invoiceDate.getFullYear() === today.getFullYear();
    if (!sameMonth || !sameYear) {
      // Dec-Jan (and general month crossover) grace period — amber up to 45 days
      // so a late-December invoice reviewed in early January isn't immediately red
      if (diffDays <= 45) return 'amber';
      return 'red';
    }
    return 'amber'; // Same month/year, just a bit old = amber
  }

  return 'none';
}

const dateWarningStyles: Record<DateWarning, React.CSSProperties> = {
  none: {},
  amber: { backgroundColor: '#fff3cd', borderColor: '#ffc107' },
  red: { backgroundColor: '#f8d7da', borderColor: '#dc3545' },
  future: { backgroundColor: '#ffe0cc', borderColor: '#e85d04', outline: '2px solid #e85d04' },
};

// Returns the day/month swapped version of a yyyy-mm-dd string, or null if invalid
function getSwappedDate(dateStr: string): string | null {
  const parts = dateStr.split('-')
  if (parts.length !== 3) return null
  const [year, month, day] = parts
  const newMonth = parseInt(day, 10)
  const newDay = parseInt(month, 10)
  if (newMonth < 1 || newMonth > 12 || newDay < 1 || newDay > 31) return null
  const swapped = `${year}-${String(newMonth).padStart(2, '0')}-${String(newDay).padStart(2, '0')}`
  const d = new Date(swapped)
  if (isNaN(d.getTime())) return null
  return swapped
}

// Returns the most plausible year-corrected version of a future date (same month/day, current or previous year).
// Only triggers when the date is >60 days in the future — invoices dated within ~2 months ahead could
// legitimately be a Dec-Jan crossover, so we don't want to aggressively suggest a year correction there.
function getYearCorrectedDate(dateStr: string): { date: string; year: number } | null {
  const parts = dateStr.split('-')
  if (parts.length !== 3) return null
  const [yearStr, month, day] = parts
  const today = new Date(); today.setHours(0, 0, 0, 0)
  const currentYear = today.getFullYear()
  if (parseInt(yearStr, 10) <= currentYear) return null // year is not wrong
  // Only suggest a year correction when clearly beyond the Dec-Jan overlap window
  const daysAhead = Math.floor((new Date(dateStr).getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
  if (daysAhead <= 60) return null
  // Try current year first, then previous year — take the first that's a valid past/today date
  for (const y of [currentYear, currentYear - 1]) {
    const candidate = `${y}-${month}-${day}`
    const d = new Date(candidate)
    if (!isNaN(d.getTime()) && d <= today) return { date: candidate, year: y }
  }
  return null
}

function LineItemsValidation({ lineItems, invoiceTotal, netTotal }: { lineItems: LineItem[]; invoiceTotal: number; netTotal: number | null }) {
  // Handle case when there are no line items
  if (lineItems.length === 0) {
    return (
      <div style={{
        marginTop: '1rem',
        padding: '1rem',
        background: '#fef2f2',
        borderRadius: '6px',
        border: '1px solid #fca5a5'
      }}>
        <div style={{ color: '#dc2626', fontWeight: '600', marginBottom: '0.5rem' }}>
          ⚠️ No Line Items
        </div>
        <div style={{ color: '#991b1b', fontSize: '0.9rem' }}>
          This invoice has no line items. It will NOT be included in GP calculations.
          Use the "Edit Line Items" button above to manually add line items.
        </div>
        <div style={{ marginTop: '0.5rem', color: '#991b1b', fontSize: '0.9rem' }}>
          <strong>Invoice Total:</strong> £{invoiceTotal.toFixed(2)}
        </div>
      </div>
    );
  }

  const lineItemsTotal = lineItems.reduce((sum, item) => sum + (item.amount || 0), 0);
  const stockItemsTotal = lineItems
    .filter(item => !item.is_non_stock)
    .reduce((sum, item) => sum + (item.amount || 0), 0);
  const nonStockItemsTotal = lineItems
    .filter(item => item.is_non_stock)
    .reduce((sum, item) => sum + (item.amount || 0), 0);

  // Line item amounts are net values, so compare against netTotal (if available)
  const compareTotal = netTotal ?? invoiceTotal;
  const difference = Math.abs(compareTotal - lineItemsTotal);
  const exactMatch = difference <= TOLERANCE;
  const isValid = exactMatch;
  const hasNonStock = nonStockItemsTotal > 0;

  return (
    <div style={{
      marginTop: '1rem',
      padding: '0.75rem',
      background: isValid ? '#d4edda' : '#fff3cd',
      borderRadius: '6px',
      border: `1px solid ${isValid ? '#c3e6cb' : '#ffeeba'}`,
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span style={{ fontWeight: '500' }}>
          Line Items Total: <strong>£{lineItemsTotal.toFixed(2)}</strong>
        </span>
        <span style={{ fontWeight: '500' }}>
          Invoice Net: <strong>£{(netTotal ?? invoiceTotal).toFixed(2)}</strong>
          {netTotal && <span style={{ fontSize: '0.85rem', color: '#666' }}> (Gross: £{invoiceTotal.toFixed(2)})</span>}
        </span>
      </div>
      {hasNonStock && (
        <div style={{ marginTop: '0.5rem', padding: '0.5rem', background: 'rgba(255,255,255,0.5)', borderRadius: '4px' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem' }}>
            <span><strong>Stock Items:</strong> £{stockItemsTotal.toFixed(2)}</span>
            <span style={{ color: '#856404' }}><strong>Non-Stock:</strong> £{nonStockItemsTotal.toFixed(2)}</span>
          </div>
          <div style={{ marginTop: '0.25rem', fontSize: '0.8rem', color: '#155724' }}>
            GP will be calculated using stock items only
          </div>
        </div>
      )}
      {exactMatch ? (
        <div style={{ marginTop: '0.5rem', fontSize: '0.85rem', color: '#155724' }}>
          ✓ Totals match
        </div>
      ) : (
        <div style={{ marginTop: '0.5rem', fontSize: '0.85rem', color: isValid ? '#155724' : '#856404' }}>
          {isValid ? '✓ ' : '⚠ '}Difference: £{difference.toFixed(2)}
        </div>
      )}
    </div>
  );
}

export default function Review() {
  const { id } = useParams()
  const { token, user } = useAuth()
  const navigate = useNavigate()
  const queryClient = useQueryClient()

  const [invoiceNumber, setInvoiceNumber] = useState('')
  const [invoiceDate, setInvoiceDate] = useState('')
  const [total, setTotal] = useState('')
  const [netTotal, setNetTotal] = useState('')
  const [category, setCategory] = useState('food')
  const [orderNumber, setOrderNumber] = useState('')
  const [documentType, setDocumentType] = useState('invoice')
  const [supplierId, setSupplierId] = useState<string>('')
  const [showDeleteModal, setShowDeleteModal] = useState(false)
  const [showDuplicateModal, setShowDuplicateModal] = useState(false)
  const [showCreateSupplierModal, setShowCreateSupplierModal] = useState(false)
  const [showDisputeModal, setShowDisputeModal] = useState(false)
  const [viewingDisputeId, setViewingDisputeId] = useState<number | null>(null)
  const [adminOperationInProgress, setAdminOperationInProgress] = useState(false)
  const [adminOperationResult, setAdminOperationResult] = useState<{type: 'success' | 'error', message: string} | null>(null)
  const [showRawOcrModal, setShowRawOcrModal] = useState(false)
  const [showSearchModal, setShowSearchModal] = useState(false)
  const [showBulkStockModal, setShowBulkStockModal] = useState(false)
  const [searchingLineItem, setSearchingLineItem] = useState<LineItem | null>(null)
  const [searchQuery, setSearchQuery] = useState('')
  const [searchResults, setSearchResults] = useState<SearchResponse | null>(null)
  const [searchLoading, setSearchLoading] = useState(false)
  const [newSupplierName, setNewSupplierName] = useState('')
  const [editingLineItem, setEditingLineItem] = useState<number | null>(null)
  const [lineItemEdits, setLineItemEdits] = useState<Partial<LineItem>>({})
  const [bulkEditMode, setBulkEditMode] = useState(false)
  const [highlightedField, setHighlightedField] = useState<string | null>(null)
  const [expandedLineItem, setExpandedLineItem] = useState<number | null>(null)
  const [costBreakdownEdits, setCostBreakdownEdits] = useState<Partial<LineItem>>({})
  const [descSwapItem, setDescSwapItem] = useState<LineItem | null>(null)  // For description swap modal
  const [portionDescription, setPortionDescription] = useState<string>('')
  const [saveAsDefault, setSaveAsDefault] = useState(false)
  const [currentDefinition, setCurrentDefinition] = useState<ProductDefinition | null>(null)
  const [definitionLoading, setDefinitionLoading] = useState(false)
  const [pdfPages, setPdfPages] = useState<{ width: number; height: number; displayWidth: number; displayHeight: number; canvas: HTMLCanvasElement }[]>([])
  const [zoomLevel, setZoomLevel] = useState<number>(1)
  const [zoomPageNum, setZoomPageNum] = useState<number>(0) // Which page the zoom applies to
  const [imageZoom, setImageZoom] = useState<number>(1) // Zoom level for non-PDF images
  const pdfContainerRef = useRef<HTMLDivElement>(null)
  const containerRef = useRef<HTMLDivElement>(null)
  const imageContainerRef = useRef<HTMLDivElement>(null)
  const highlightRef = useRef<HTMLDivElement>(null)
  // Dext integration state
  const [invoiceNotes, setInvoiceNotes] = useState('')
  const [showDextSendConfirm, setShowDextSendConfirm] = useState(false)
  const [showLinkDisputeModal, setShowLinkDisputeModal] = useState(false)
  // Date picker modal state
  const [showDatePickerModal, setShowDatePickerModal] = useState(false)
  const [parsedDates, setParsedDates] = useState<Array<{ date: string; original: string; context: string; bbox?: { x: number; y: number; width: number; height: number; pageNumber: number } }>>([])
  const [parsedDatesLoading, setParsedDatesLoading] = useState(false)
  // Non-stock confirmation modal state
  const [showNonStockConfirmModal, setShowNonStockConfirmModal] = useState(false)
  const [nonStockConflictItems, setNonStockConflictItems] = useState<Array<{ description: string | null; amount: number | null }>>([])
  // Invoice number search modal state
  const [showInvoiceNumberModal, setShowInvoiceNumberModal] = useState(false)
  const [invoiceNumberCandidates, setInvoiceNumberCandidates] = useState<Array<{ value: string; context: string; source: string; bbox?: { x: number; y: number; width: number; height: number; pageNumber: number } }>>([])
  const [invoiceNumberExamples, setInvoiceNumberExamples] = useState<string[]>([])
  const [invoiceNumberLoading, setInvoiceNumberLoading] = useState(false)
  // Line items sorting and filtering
  const [lineItemSortColumn, setLineItemSortColumn] = useState<string>('')
  const [lineItemSortDirection, setLineItemSortDirection] = useState<'asc' | 'desc'>('asc')
  const [lineItemPriceFilter, setLineItemPriceFilter] = useState<string>('')
  const [lineItemSearchText, setLineItemSearchText] = useState<string>('')
  const [lineItemPortionsFilter, setLineItemPortionsFilter] = useState<string>('')
  const [lineItemMissingDataFilter, setLineItemMissingDataFilter] = useState<string>('')
  // Current visible page for sticky indicator (multi-page invoices)
  const [currentVisiblePage, setCurrentVisiblePage] = useState<number>(1)
  const lineItemsTableRef = useRef<HTMLDivElement>(null)

  // Ingredient mapping modal state
  const [ingredientModalItem, setIngredientModalItem] = useState<LineItem | null>(null)
  const [ingredientSearch, setIngredientSearch] = useState('')
  const [ingredientSuggestions, setIngredientSuggestions] = useState<IngredientSuggestion[]>([])
  const [ingredientSearchLoading, setIngredientSearchLoading] = useState(false)
  const [selectedIngredientId, setSelectedIngredientId] = useState<number | null>(null)
  const [selectedIngredientName, setSelectedIngredientName] = useState('')
  const [selectedIngredientUnit, setSelectedIngredientUnit] = useState('')
  const [showCreateIngredient, setShowCreateIngredient] = useState(false)
  const [showLegacyPortioning, setShowLegacyPortioning] = useState(false)
  const [ingredientConversionDisplay, setIngredientConversionDisplay] = useState('')

  // Description alias suggestion state
  const [aliasSuggestions, setAliasSuggestions] = useState<Record<string, {
    description: string
    source_id: number
    ingredient_id: number
    ingredient_name: string | null
    canonical_description: string
    product_code: string | null
    similarity: number
    price_difference: number | null
  }>>({})
  const [addingAliasFor, setAddingAliasFor] = useState<string | null>(null)

  // LLM FEATURE — AI Assist state — see LLM-MANIFEST.md for removal instructions
  const [aiMatchLoading, setAiMatchLoading] = useState(false)
  const [aiMatchResults, setAiMatchResults] = useState<Array<{ id: number; name: string; confidence: number; reason: string }>>([])
  const [aiAssistLoading, setAiAssistLoading] = useState(false)
  const [aiAssistSuggestions, setAiAssistSuggestions] = useState<AiAssistSuggestions | null>(null)
  const [aiReconciliationMatches, setAiReconciliationMatches] = useState<AiReconciliationMatch[]>([])
  const [aiAssistError, setAiAssistError] = useState<string | null>(null)
  const [aiDismissedCorrections, setAiDismissedCorrections] = useState<Set<string>>(new Set())
  const [aiPackSizeLoading, setAiPackSizeLoading] = useState(false)
  const [aiPackSizeSource, setAiPackSizeSource] = useState<string | null>(null)

  // Price history modal state
  const [historyModal, setHistoryModal] = useState<{
    isOpen: boolean
    productCode: string | null
    description: string | null
    unit: string | null
    supplierId: number
    supplierName: string
    currentPrice?: number
    sourceInvoiceId?: number
    sourceLineItemId?: number
  } | null>(null)

  // Purchase Order matching state
  const [viewingPoId, setViewingPoId] = useState<number | null>(null)

  const { data: invoice, isLoading, refetch: refetchInvoice } = useQuery<Invoice>({
    queryKey: ['invoice', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch invoice')
      return res.json()
    },
    staleTime: 30000,
    refetchOnWindowFocus: false,
  })

  // Direct URL with token - simpler approach
  const imageUrl = invoice
    ? `/kitchen/api/invoices/${id}/file#toolbar=0&navpanes=0&view=FitH`
    : null

  const { data: lineItems, refetch: refetchLineItems } = useQuery<LineItem[]>({
    queryKey: ['invoice-line-items', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch line items')
      return res.json()
    },
  })

  const { data: stockHistory } = useQuery<Record<string, {
    has_history: boolean
    previously_non_stock: boolean
    total_occurrences: number
    non_stock_occurrences: number
    most_recent_status: boolean | null
  }>>({
    queryKey: ['invoice-stock-history', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}/stock-history`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch stock history')
      return res.json()
    },
    enabled: !!id,
  })

  const { data: duplicateInfo } = useQuery<DuplicateCompare>({
    queryKey: ['invoice-duplicates', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}/duplicates`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch duplicates')
      return res.json()
    },
    enabled: !!invoice?.duplicate_status,
  })

  const { data: rawOcrData } = useQuery<{ raw_json: any; raw_text: string }>({
    queryKey: ['invoice-ocr-data', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}/ocr-data`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch OCR data')
      return res.json()
    },
  })

  // Fetch settings for high quantity threshold
  const { data: settings } = useQuery<Settings>({
    queryKey: ['settings'],
    queryFn: async () => {
      const res = await fetch('/kitchen/api/settings/', {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to fetch settings')
      return res.json()
    },
    staleTime: 60000, // Cache for 1 minute
  })

  // Fetch description alias suggestions for unmapped line items without product codes
  useEffect(() => {
    if (!lineItems || !supplierId || !token) return
    const unmappedItems = lineItems.filter(li => !li.ingredient_id && !li.product_code && li.description)
    if (unmappedItems.length === 0) {
      setAliasSuggestions({})
      return
    }
    const items = unmappedItems.map(li => ({
      description: li.description!.split('\n')[0].trim(),
      price: li.unit_price ?? undefined,
    }))
    // Deduplicate by description
    const uniqueItems = Array.from(new Map(items.map(i => [i.description.toLowerCase(), i])).values())
    if (uniqueItems.length === 0) return

    fetch('/kitchen/api/ingredients/sources/alias-suggestions', {
      method: 'POST',
      credentials: 'include', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ supplier_id: parseInt(supplierId), items: uniqueItems }),
    })
      .then(res => res.ok ? res.json() : [])
      .then((suggestions: any[]) => {
        const map: typeof aliasSuggestions = {}
        for (const s of suggestions) {
          map[s.description.toLowerCase()] = s
        }
        setAliasSuggestions(map)
      })
      .catch(() => setAliasSuggestions({}))
  }, [lineItems, supplierId, token])

  // Helper to get bounding box for a field from raw OCR data
  const getFieldBoundingBox = (fieldName: string): { x: number; y: number; width: number; height: number; pageNumber: number } | null => {
    if (!rawOcrData?.raw_json?.documents?.[0]?.fields?.[fieldName]?.bounding_regions?.[0]) {
      return null
    }
    const region = rawOcrData.raw_json.documents[0].fields[fieldName].bounding_regions[0]
    const polygon = region.polygon
    const pageNumber = region.page_number || 1 // Azure uses 1-based page numbers
    if (!polygon || polygon.length < 4) return null

    // Get page dimensions for the correct page (Azure uses inches by default)
    const pageInfo = rawOcrData.raw_json.pages?.[pageNumber - 1] // Convert to 0-based
    const pageWidth = pageInfo?.width || 8.5
    const pageHeight = pageInfo?.height || 11

    // Convert polygon to bounding box (polygon is array of [x, y] pairs)
    const xs = polygon.map((p: number[]) => p[0])
    const ys = polygon.map((p: number[]) => p[1])
    const minX = Math.min(...xs)
    const maxX = Math.max(...xs)
    const minY = Math.min(...ys)
    const maxY = Math.max(...ys)

    // Return as percentages of page dimensions for easy scaling
    return {
      x: (minX / pageWidth) * 100,
      y: (minY / pageHeight) * 100,
      width: ((maxX - minX) / pageWidth) * 100,
      height: ((maxY - minY) / pageHeight) * 100,
      pageNumber,
    }
  }

  // Helper to get bounding box for a line item by index
  const getLineItemBoundingBox = (lineIndex: number): { x: number; y: number; width: number; height: number; pageNumber: number } | null => {
    if (!rawOcrData?.raw_json?.documents?.[0]?.fields?.Items?.value?.[lineIndex]?.bounding_regions?.[0]) {
      return null
    }
    const region = rawOcrData.raw_json.documents[0].fields.Items.value[lineIndex].bounding_regions[0]
    const polygon = region.polygon
    const pageNumber = region.page_number || 1
    if (!polygon || polygon.length < 4) return null

    const pageInfo = rawOcrData.raw_json.pages?.[pageNumber - 1]
    const pageWidth = pageInfo?.width || 8.5
    const pageHeight = pageInfo?.height || 11

    const xs = polygon.map((p: number[]) => p[0])
    const ys = polygon.map((p: number[]) => p[1])
    const minX = Math.min(...xs)
    const maxX = Math.max(...xs)
    const minY = Math.min(...ys)
    const maxY = Math.max(...ys)

    return {
      x: (minX / pageWidth) * 100,
      y: (minY / pageHeight) * 100,
      width: ((maxX - minX) / pageWidth) * 100,
      height: ((maxY - minY) / pageHeight) * 100,
      pageNumber,
    }
  }

  // Crop a bounding-box region from the high-res PDF canvas and return a data URL.
  // Returns null for non-PDF documents or if the page hasn't rendered yet.
  const cropBboxToDataUrl = (
    bbox: { x: number; y: number; width: number; height: number; pageNumber: number },
    options?: { padX?: number; padY?: number; strokeColor?: string }
  ): string | null => {
    if (!invoice?.image_path?.toLowerCase().endsWith('.pdf') || pdfPages.length === 0) return null
    const pageData = pdfPages[bbox.pageNumber - 1]
    if (!pageData) return null
    const { padX = 60, padY = 20, strokeColor = '#007bff' } = options || {}
    const bboxX = (bbox.x / 100) * pageData.width
    const bboxY = (bbox.y / 100) * pageData.height
    const bboxW = (bbox.width / 100) * pageData.width
    const bboxH = (bbox.height / 100) * pageData.height
    const startX = Math.max(0, bboxX - padX)
    const startY = Math.max(0, bboxY - padY)
    const endX = Math.min(pageData.width, bboxX + bboxW + padX)
    const endY = Math.min(pageData.height, bboxY + bboxH + padY)
    const cropW = endX - startX
    const cropH = endY - startY
    const croppedCanvas = document.createElement('canvas')
    croppedCanvas.width = cropW
    croppedCanvas.height = cropH
    const ctx = croppedCanvas.getContext('2d')
    if (!ctx) return null
    ctx.drawImage(pageData.canvas, startX, startY, cropW, cropH, 0, 0, cropW, cropH)
    ctx.strokeStyle = strokeColor
    ctx.lineWidth = 3
    ctx.strokeRect(bboxX - startX - 4, bboxY - startY - 4, bboxW + 8, bboxH + 8)
    return croppedCanvas.toDataURL()
  }

  // Calculate zoom level to make bounding box fill ~80% of container
  const calculateZoomForBbox = (bbox: { x: number; y: number; width: number; height: number; pageNumber: number } | null): number => {
    if (!bbox || !pdfContainerRef.current || pdfPages.length === 0) return 2

    const pageData = pdfPages[bbox.pageNumber - 1]
    if (!pageData) return 2

    // Get container dimensions
    const containerWidth = pdfContainerRef.current.clientWidth - 32 // padding
    const containerHeight = pdfContainerRef.current.clientHeight - 32

    // Calculate bbox size in display pixels
    const bboxDisplayWidth = (bbox.width / 100) * pageData.displayWidth
    const bboxDisplayHeight = (bbox.height / 100) * pageData.displayHeight

    // Calculate zoom to make bbox fill 80% of container (use the more constraining dimension)
    const targetFill = 0.8
    const zoomForWidth = (containerWidth * targetFill) / bboxDisplayWidth
    const zoomForHeight = (containerHeight * targetFill) / bboxDisplayHeight

    // Use the smaller zoom so it fits both dimensions, with min/max limits
    const zoom = Math.min(zoomForWidth, zoomForHeight)
    return Math.max(2, Math.min(zoom, 8)) // Clamp between 2x and 8x
  }

  // Scroll to page containing the bounding box and set zoom with centering
  const scrollToHighlight = (bbox: { x: number; y: number; width: number; height: number; pageNumber: number } | null, zoom: number) => {
    if (!bbox || !pdfContainerRef.current || pdfPages.length === 0) return

    const pageData = pdfPages[bbox.pageNumber - 1]
    if (!pageData) return

    // Set zoom level for target page directly
    setZoomLevel(zoom)
    setZoomPageNum(bbox.pageNumber)

    // Wait for DOM update, then scroll to centered position
    setTimeout(() => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          if (!pdfContainerRef.current) return

          const container = pdfContainerRef.current

          // Force layout recalculation
          void container.offsetHeight

          // Find the actual page element to get its real position
          const pageElements = container.querySelectorAll('[data-page-container]')
          const targetPageElement = pageElements[bbox.pageNumber - 1] as HTMLElement

          if (!targetPageElement) {
            console.warn('Could not find page element for scrolling', {
              totalPages: pdfPages.length,
              targetPage: bbox.pageNumber,
              foundElements: pageElements.length
            })
            return
          }

          // Use getBoundingClientRect for accurate positioning
          const containerRect = container.getBoundingClientRect()
          const pageRect = targetPageElement.getBoundingClientRect()

          // Get the actual dimensions of the zoomed page
          const pageActualWidth = pageRect.width
          const pageActualHeight = pageRect.height

          // Calculate bbox center position in pixels (bbox coords are percentages of the page)
          const bboxCenterX = ((bbox.x + bbox.width / 2) / 100) * pageActualWidth
          const bboxCenterY = ((bbox.y + bbox.height / 2) / 100) * pageActualHeight

          // Calculate bbox center position relative to the container
          // pageRect.left/top are relative to viewport, containerRect.left/top are container position
          // Add current scroll position to get absolute position within scrollable content
          const pageLeftInContainer = pageRect.left - containerRect.left + container.scrollLeft
          const pageTopInContainer = pageRect.top - containerRect.top + container.scrollTop

          const bboxAbsoluteX = pageLeftInContainer + bboxCenterX
          const bboxAbsoluteY = pageTopInContainer + bboxCenterY

          // Container viewport dimensions (clientWidth/Height excludes scrollbars but includes padding)
          const containerWidth = container.clientWidth
          const containerHeight = container.clientHeight

          // Calculate scroll position to center the bbox in the viewport
          const scrollLeft = Math.max(0, bboxAbsoluteX - containerWidth / 2)
          const scrollTop = Math.max(0, bboxAbsoluteY - containerHeight / 2)

          // Scroll instantly to show the highlighted area centered
          container.scrollTo({
            left: scrollLeft,
            top: scrollTop,
            behavior: 'auto' // Instant scroll instead of smooth
          })
        })
      })
    }, 100) // Wait for zoom to apply and DOM to update
  }

  // Reset zoom
  const resetZoom = () => {
    setZoomLevel(1)
    setZoomPageNum(0)
    if (pdfContainerRef.current) {
      pdfContainerRef.current.scrollTo({ left: 0, top: 0, behavior: 'smooth' })
    }
  }

  // Handler to toggle highlight and scroll to it
  const handleHighlightField = (fieldName: string) => {
    if (highlightedField === fieldName) {
      setHighlightedField(null)
      resetZoom()
    } else {
      setHighlightedField(fieldName)
      setExpandedLineItem(null) // Clear any line item highlight
      const bbox = getFieldBoundingBox(fieldName)
      const dynamicZoom = calculateZoomForBbox(bbox)
      scrollToHighlight(bbox, dynamicZoom)
    }
  }

  // Handler to toggle line item inline preview (no zoom/scroll for line items)
  const handleHighlightLineItem = (itemId: number, _lineIndex: number) => {
    if (expandedLineItem === itemId) {
      setExpandedLineItem(null)
    } else {
      setExpandedLineItem(itemId)
      setHighlightedField(null) // Clear any field highlight
      resetZoom() // Reset zoom when showing inline preview
    }
  }

  const { data: suppliers } = useQuery<Supplier[]>({
    queryKey: ['suppliers'],
    queryFn: async () => {
      const res = await fetch('/kitchen/api/suppliers/', {
        credentials: 'include',
      })
      if (!res.ok) return []
      return res.json()
    },
  })

  // PO matching query — find linked or matching POs for this invoice
  const { data: poMatchData, refetch: refetchPoMatch } = useQuery<{
    matches: Array<{
      po_id: number
      order_date: string | null
      total_amount: number | null
      order_reference: string | null
      status: string
      order_type: string
      confidence: number
    }>
    linked_po: {
      po_id: number
      order_date: string | null
      total_amount: number | null
      order_reference: string | null
      status: string
      order_type: string
    } | null
  }>({
    queryKey: ['po-match', id],
    queryFn: async () => {
      const res = await fetch(`/kitchen/api/purchase-orders/matching/for-invoice?invoice_id=${id}`, {
        credentials: 'include',
      })
      if (!res.ok) return { matches: [], linked_po: null }
      return res.json()
    },
    enabled: !!invoice?.supplier_id,
  })

  useEffect(() => {
    if (invoice) {
      setInvoiceNumber(invoice.invoice_number || '')
      setInvoiceDate(invoice.invoice_date || '')
      setTotal(invoice.total?.toString() || '')
      setNetTotal(invoice.net_total?.toString() || '')
      setCategory(invoice.category || 'food')
      setOrderNumber(invoice.order_number || '')
      setDocumentType(invoice.document_type || 'invoice')
      setSupplierId(invoice.supplier_id?.toString() || '')
      setInvoiceNotes(invoice.notes || '')
    }
  }, [invoice])

  // Auto-reload when a PENDING invoice finishes processing
  useEffect(() => {
    if (!invoice || invoice.status !== 'PENDING') return

    const pollInterval = setInterval(async () => {
      try {
        const checkRes = await fetch(`/kitchen/api/invoices/${id}`, {
          credentials: 'include',
        })
        if (checkRes.ok) {
          const inv = await checkRes.json()
          if (inv.status !== 'PENDING') {
            clearInterval(pollInterval)
            window.location.reload()
          }
        }
      } catch {
        // Ignore polling errors
      }
    }, 2000)

    return () => clearInterval(pollInterval)
  }, [invoice?.status, id, token])

  // Render all PDF pages to canvases for highlighting support
  useEffect(() => {
    const renderPdf = async () => {
      if (!invoice || !token) return
      const isPdf = invoice.image_path?.toLowerCase().endsWith('.pdf')
      if (!isPdf || !containerRef.current) return

      try {
        // Fetch the PDF
        const pdfUrl = `/kitchen/api/invoices/${id}/file`
        const response = await fetch(pdfUrl, { credentials: 'include' })
        const arrayBuffer = await response.arrayBuffer()

        // Load the PDF document
        const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
        const numPages = pdf.numPages

        // Get container width for display scaling
        // Account for: imageSection padding (48px) + pdfScrollContainer padding (32px) + extra margin (32px) = 112px total
        const containerWidth = containerRef.current.clientWidth - 112

        // Render at high fixed resolution for quality (matches upload max of 2000px)
        const targetRenderWidth = 1500 // High quality render width

        // Render all pages
        const pages: { width: number; height: number; displayWidth: number; displayHeight: number; canvas: HTMLCanvasElement }[] = []

        for (let pageNum = 1; pageNum <= numPages; pageNum++) {
          const page = await pdf.getPage(pageNum)
          const viewport = page.getViewport({ scale: 1 })

          // Calculate display size for container fit
          const displayScale = containerWidth / viewport.width
          const displayWidth = viewport.width * displayScale
          const displayHeight = viewport.height * displayScale

          // Render at high resolution (independent of display size)
          const renderScale = targetRenderWidth / viewport.width
          const renderViewport = page.getViewport({ scale: renderScale })

          // Create canvas for this page at high resolution
          const canvas = document.createElement('canvas')
          canvas.width = renderViewport.width
          canvas.height = renderViewport.height

          const context = canvas.getContext('2d')
          if (context) {
            // If pdf_preview_show_annotations is false, hide PDF annotations
            const annotationMode = settings?.pdf_preview_show_annotations === false
              ? AnnotationMode.DISABLE
              : AnnotationMode.ENABLE
            await page.render({
              canvasContext: context,
              viewport: renderViewport,
              canvas: canvas,
              annotationMode: annotationMode,
            }).promise
          }

          pages.push({
            width: renderViewport.width,  // High-res canvas dimensions
            height: renderViewport.height,
            displayWidth,  // Display dimensions (half of canvas for 2x)
            displayHeight,
            canvas,
          })
        }

        setPdfPages(pages)
      } catch (err) {
        console.error('Error rendering PDF:', err)
      }
    }

    renderPdf()
  }, [invoice, token, id, settings?.pdf_preview_show_annotations])

  const updateMutation = useMutation({
    mutationFn: async (data: Partial<Invoice>) => {
      const res = await fetch(`/kitchen/api/invoices/${id}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      })
      if (!res.ok) {
        const errorData = await res.json().catch(() => ({}))
        const errorMessage = errorData.detail?.message || errorData.detail || 'Failed to update'
        const error = new Error(errorMessage) as Error & { code?: string }
        error.code = errorData.detail?.error
        throw error
      }
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
      queryClient.invalidateQueries({ queryKey: ['invoices'] })
      queryClient.invalidateQueries({ queryKey: ['invoice-line-items', id] })
    },
  })

  const deleteMutation = useMutation({
    mutationFn: async () => {
      const res = await fetch(`/kitchen/api/invoices/${id}`, {
        method: 'DELETE',
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to delete')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['invoices'] })
      navigate('/invoices')
    },
  })

  const updateLineItemMutation = useMutation({
    mutationFn: async ({ itemId, data }: { itemId: number; data: Partial<LineItem> }) => {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      })
      if (!res.ok) throw new Error('Failed to update')
      return res.json()
    },
    onSuccess: () => {
      refetchLineItems()
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })  // Refresh stock_total
      setEditingLineItem(null)
      setLineItemEdits({})
      setExpandedLineItem(null)  // Close line item preview
    },
  })

  const createLineItemMutation = useMutation({
    mutationFn: async (data: Partial<LineItem>) => {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      })
      if (!res.ok) throw new Error('Failed to create line item')
      return res.json()
    },
    onSuccess: () => {
      refetchLineItems()
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
      setEditingLineItem(null)
      setLineItemEdits({})
    },
  })

  const deleteLineItemMutation = useMutation({
    mutationFn: async (itemId: number) => {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
        method: 'DELETE',
        credentials: 'include',
        headers: {
        },
      })
      if (!res.ok) throw new Error('Failed to delete line item')
      return res.json()
    },
    onSuccess: () => {
      refetchLineItems()
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
      setEditingLineItem(null)
    },
  })

  const saveDefinitionMutation = useMutation({
    mutationFn: async ({ itemId, portionDesc }: { itemId: number; portionDesc?: string }) => {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}/save-definition`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ portion_description: portionDesc || null }),
      })
      if (!res.ok) {
        const err = await res.json()
        throw new Error(err.detail || 'Failed to save definition')
      }
      return res.json()
    },
  })

  const createSupplierMutation = useMutation({
    mutationFn: async (name: string) => {
      const res = await fetch('/kitchen/api/suppliers/', {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name }),
      })
      if (!res.ok) throw new Error('Failed to create supplier')
      return res.json()
    },
    onSuccess: (newSupplier: { id: number; name: string }) => {
      queryClient.invalidateQueries({ queryKey: ['suppliers'] })
      setSupplierId(newSupplier.id.toString())
      setShowCreateSupplierModal(false)
      setNewSupplierName('')
    },
  })

  const addAliasMutation = useMutation({
    mutationFn: async ({ supplierId, alias, invoiceId }: { supplierId: number; alias: string; invoiceId?: number }) => {
      const res = await fetch(`/kitchen/api/suppliers/${supplierId}/aliases`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ alias, invoice_id: invoiceId }),
      })
      if (!res.ok) throw new Error('Failed to add alias')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['suppliers'] })
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
    },
  })

  const addDescriptionAliasMutation = useMutation({
    mutationFn: async ({ sourceId, alias }: { sourceId: number; alias: string }) => {
      const res = await fetch(`/kitchen/api/ingredients/sources/${sourceId}/aliases`, {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ alias }),
      })
      if (!res.ok) throw new Error('Failed to add alias')
      return res.json()
    },
    onSuccess: (_data, variables) => {
      // Remove this suggestion from the map
      setAliasSuggestions(prev => {
        const next = { ...prev }
        delete next[variables.alias.toLowerCase()]
        return next
      })
      setAddingAliasFor(null)
      // Refresh line items (descriptions will have been renamed)
      queryClient.invalidateQueries({ queryKey: ['invoice-line-items', id] })
    },
  })

  // PO link/unlink handlers
  const handleLinkPo = async (poId: number) => {
    try {
      const res = await fetch(`/kitchen/api/purchase-orders/${poId}/link`, {
        method: 'POST',
        credentials: 'include', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ invoice_id: parseInt(id!) }),
      })
      if (!res.ok) throw new Error('Failed to link PO')
      refetchPoMatch()
    } catch {
      // silently fail, user can retry
    }
  }

  const handleUnlinkPo = async (poId: number) => {
    try {
      const res = await fetch(`/kitchen/api/purchase-orders/${poId}/unlink`, {
        method: 'POST',
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to unlink PO')
      refetchPoMatch()
    } catch {
      // silently fail
    }
  }

  const handleCreateSupplier = () => {
    if (newSupplierName.trim()) {
      createSupplierMutation.mutate(newSupplierName.trim())
    }
  }

  const openCreateSupplierModal = () => {
    setNewSupplierName(invoice?.vendor_name || '')
    setShowCreateSupplierModal(true)
  }

  const handleSave = async (status: string = 'REVIEWED') => {
    await updateMutation.mutateAsync({
      invoice_number: invoiceNumber || null,
      invoice_date: invoiceDate || null,
      total: total ? parseFloat(total) : null,
      net_total: netTotal ? parseFloat(netTotal) : null,
      supplier_id: supplierId ? parseInt(supplierId) : null,
      category,
      order_number: orderNumber || null,
      document_type: documentType,
      status,
    })
  }

  // Auto-save when invoice details change (debounced)
  useEffect(() => {
    if (!invoice) return

    const timeoutId = setTimeout(() => {
      // Only auto-save if values have actually changed from the invoice
      const hasChanges =
        invoiceNumber !== (invoice.invoice_number || '') ||
        invoiceDate !== (invoice.invoice_date || '') ||
        total !== (invoice.total?.toString() || '') ||
        netTotal !== (invoice.net_total?.toString() || '') ||
        supplierId !== (invoice.supplier_id?.toString() || '') ||
        category !== (invoice.category || 'food') ||
        orderNumber !== (invoice.order_number || '') ||
        documentType !== (invoice.document_type || 'invoice')

      if (hasChanges) {
        updateMutation.mutate({
          invoice_number: invoiceNumber || null,
          invoice_date: invoiceDate || null,
          total: total ? parseFloat(total) : null,
          net_total: netTotal ? parseFloat(netTotal) : null,
          supplier_id: supplierId ? parseInt(supplierId) : null,
          category,
          order_number: orderNumber || null,
          document_type: documentType,
        })
      }
    }, 1000) // 1 second debounce

    return () => clearTimeout(timeoutId)
  }, [invoiceNumber, invoiceDate, total, netTotal, supplierId, category, orderNumber, documentType])

  const handleConfirm = async () => {
    // Warn (but allow) if there are open disputes
    if (invoice?.has_open_disputes) {
      if (!window.confirm(
        `This invoice has ${invoice.dispute_count} open dispute(s).\n\n` +
        `You can still confirm — the invoice will appear in reports while the dispute is tracked separately.\n\n` +
        `Continue?`
      )) {
        return
      }
    }

    // Warn if invoice date is in the future — likely a day/month swap
    if (invoiceDate && getDateWarning(invoiceDate, 'PENDING') === 'future') {
      const futureDate = new Date(invoiceDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
      if (!window.confirm(
        `⚠ Invoice date is in the future: ${futureDate}\n\n` +
        `This is almost certainly a day/month format error (e.g. 6 April entered as 4 June).\n\n` +
        `Please double-check the date on the document before confirming.\n\n` +
        `Confirm anyway?`
      )) {
        return
      }
    }

    // Warn if any items were previously marked non-stock but aren't on this invoice
    const nonStockConflicts = (lineItems || []).filter(item => {
      const history = stockHistory?.[item.id.toString()]
      return history?.has_history && history.previously_non_stock && !item.is_non_stock
    })
    if (nonStockConflicts.length > 0) {
      setNonStockConflictItems(nonStockConflicts.map(item => ({
        description: item.description,
        amount: item.amount,
      })))
      setShowNonStockConfirmModal(true)
      return
    }

    // Check for invoice date before confirming
    if (!invoiceDate) {
      alert(
        `Cannot confirm invoice without a date.\n\n` +
        `Please set the invoice date before confirming.\n\n` +
        `Invoices without dates cannot be found in date-filtered views.`
      )
      return
    }

    await doConfirm()
  }

  const doConfirm = async () => {
    try {
      await handleSave('CONFIRMED')
      navigate('/invoices')
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Failed to confirm invoice'
      alert(message)
    }
  }

  const handleDelete = () => {
    deleteMutation.mutate()
  }

  const handleMarkDextSent = async () => {
    if (!window.confirm('Mark this invoice as sent to Dext without actually sending?\n\nThis will also trigger Nextcloud archival if configured.')) {
      return
    }

    setAdminOperationInProgress(true)
    setAdminOperationResult(null)

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/mark-dext-sent`, {
        method: 'POST',
        credentials: 'include',
      })

      if (!res.ok) {
        const error = await res.json()
        throw new Error(error.detail || 'Failed to mark as sent')
      }

      const result = await res.json()
      setAdminOperationResult({
        type: 'success',
        message: result.message + (result.archival_status ? `\n\n${result.archival_status}` : '')
      })

      // Refresh invoice data
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
    } catch (error) {
      setAdminOperationResult({
        type: 'error',
        message: error instanceof Error ? error.message : 'Operation failed'
      })
    } finally {
      setAdminOperationInProgress(false)
    }
  }

  const handleReprocessOCR = async () => {
    if (!window.confirm('Reprocess existing OCR data?\n\nThis will:\n- Re-identify supplier\n- Re-detect document type\n- Re-create line items with product definitions\n- Re-run duplicate detection\n\nExisting line items will be replaced.')) {
      return
    }

    setAdminOperationInProgress(true)
    setAdminOperationResult(null)

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/reprocess`, {
        method: 'POST',
        credentials: 'include',
      })

      if (!res.ok) {
        const error = await res.json()
        throw new Error(error.detail || 'Failed to reprocess')
      }

      const result = await res.json()
      setAdminOperationResult({
        type: 'success',
        message: `${result.message}\n\nSupplier ID: ${result.supplier_id || 'None'}\nDocument Type: ${result.document_type}\nLine Items: ${result.line_items_count}\nDuplicate Status: ${result.duplicate_status || 'None'}`
      })

      // Full page reload to ensure all data is fresh
      window.location.reload()
    } catch (error) {
      setAdminOperationResult({
        type: 'error',
        message: error instanceof Error ? error.message : 'Operation failed'
      })
    } finally {
      setAdminOperationInProgress(false)
    }
  }

  const handleFetchParsedDates = async () => {
    setParsedDatesLoading(true)
    setParsedDates([])
    setShowDatePickerModal(true)

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/parse-dates`, {
        credentials: 'include',
      })

      if (!res.ok) {
        throw new Error('Failed to parse dates')
      }

      const data = await res.json()
      setParsedDates(data.found_dates || [])
    } catch (error) {
      console.error('Failed to fetch parsed dates:', error)
    } finally {
      setParsedDatesLoading(false)
    }
  }

  const handleSelectParsedDate = (dateStr: string) => {
    setInvoiceDate(dateStr)
    setShowDatePickerModal(false)
  }

  const handleFetchInvoiceNumberCandidates = async () => {
    setInvoiceNumberLoading(true)
    setInvoiceNumberCandidates([])
    setInvoiceNumberExamples([])
    setShowInvoiceNumberModal(true)
    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/parse-invoice-number`, {
        credentials: 'include',
      })
      if (!res.ok) throw new Error('Failed to search for invoice number')
      const data = await res.json()
      setInvoiceNumberCandidates(data.candidates || [])
      setInvoiceNumberExamples(data.supplier_examples || [])
    } catch (error) {
      console.error('Failed to fetch invoice number candidates:', error)
    } finally {
      setInvoiceNumberLoading(false)
    }
  }

  const handleSelectInvoiceNumber = (value: string) => {
    setInvoiceNumber(value)
    setShowInvoiceNumberModal(false)
  }

  const handleResendToAzure = async () => {
    if (!window.confirm('Re-send invoice to Azure for OCR extraction?\n\nThis will:\n- Fully re-extract data from Azure\n- Update all invoice fields\n- Re-create line items with product definitions\n- Re-run duplicate detection\n\nExisting data will be replaced.')) {
      return
    }

    setAdminOperationInProgress(true)
    setAdminOperationResult(null)

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/resend-to-azure`, {
        method: 'POST',
        credentials: 'include',
      })

      if (!res.ok) {
        const error = await res.json()
        throw new Error(error.detail || 'Failed to resend to Azure')
      }

      const result = await res.json()
      setAdminOperationResult({
        type: 'success',
        message: `${result.message}\n\nThe page will reload automatically when processing completes.`
      })

      // Poll for completion
      const pollInterval = setInterval(async () => {
        try {
          const checkRes = await fetch(`/kitchen/api/invoices/${id}`, {
            credentials: 'include',
          })
          if (checkRes.ok) {
            const inv = await checkRes.json()
            if (inv.status !== 'PENDING') {
              clearInterval(pollInterval)
              // Full page reload to ensure all data is fresh
              window.location.reload()
            }
          }
        } catch (e) {
          // Ignore polling errors
        }
      }, 2000)

      // Stop polling after 2 minutes
      setTimeout(() => clearInterval(pollInterval), 120000)
    } catch (error) {
      setAdminOperationResult({
        type: 'error',
        message: error instanceof Error ? error.message : 'Operation failed'
      })
    } finally {
      setAdminOperationInProgress(false)
    }
  }

  const handleRegenerateHighlights = async () => {
    setAdminOperationInProgress(true)
    setAdminOperationResult(null)

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/regenerate-highlights`, {
        method: 'POST',
        credentials: 'include',
      })

      if (!res.ok) {
        const error = await res.json()
        throw new Error(error.detail || 'Failed to regenerate highlights')
      }

      const result = await res.json()
      const nonStockCount = result.non_stock_count || 0
      const hasNotes = result.has_notes
      let message = ''
      if (nonStockCount > 0) {
        message = `PDF highlights regenerated successfully.\n\nHighlighted ${nonStockCount} non-stock item(s).`
      } else {
        message = 'PDF highlights cleared (no non-stock items marked).'
      }
      if (hasNotes) {
        message += '\nNotes overlay added.'
      }
      setAdminOperationResult({
        type: 'success',
        message
      })

      // Refresh invoice to update PDF preview
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
    } catch (error) {
      setAdminOperationResult({
        type: 'error',
        message: error instanceof Error ? error.message : 'Operation failed'
      })
    } finally {
      setAdminOperationInProgress(false)
    }
  }

  const startEditLineItem = (item: LineItem) => {
    setEditingLineItem(item.id)
    setLineItemEdits({
      product_code: item.product_code,
      description: item.description,
      unit: item.unit,
      quantity: item.quantity,
      unit_price: item.unit_price,
      tax_rate: item.tax_rate,
      amount: item.amount,
      is_non_stock: item.is_non_stock,
    })
    // Open line item preview
    setExpandedLineItem(item.id)
    setHighlightedField(null)
    resetZoom()
  }

  const saveLineItemEdit = (itemId: number) => {
    if (itemId === -1) {
      // This is a new line item - use POST
      createLineItemMutation.mutate(lineItemEdits)
    } else {
      // Existing line item - use PATCH
      updateLineItemMutation.mutate({ itemId, data: lineItemEdits })
    }
    // Clear edit state immediately after save
    setEditingLineItem(null)
    setLineItemEdits({})
    setExpandedLineItem(null)
  }

  const cancelEditLineItem = () => {
    setEditingLineItem(null)
    setLineItemEdits({})
    setExpandedLineItem(null)
  }

  const handleAddLineItem = () => {
    // Create a temporary new line item with placeholder ID
    const newItemId = -1  // Negative ID indicates unsaved

    // Set editing state to new item
    setEditingLineItem(newItemId)
    setLineItemEdits({
      product_code: '',
      description: '',
      unit: '',
      quantity: null,
      unit_price: null,
      tax_rate: '',
      amount: null,
      is_non_stock: false,
      line_number: (filteredAndSortedLineItems.length || 0) + 1
    })
  }

  const handleDeleteLineItem = (itemId: number) => {
    if (window.confirm('Are you sure you want to delete this line item? This action cannot be undone.')) {
      deleteLineItemMutation.mutate(itemId)
    }
  }

  // Helper to ensure item is being edited when user interacts with fields in bulk mode
  const ensureEditing = (item: LineItem) => {
    if (editingLineItem !== item.id) {
      startEditLineItem(item)
    }
  }

  // Build dynamic list of supplier words from loaded suppliers
  const getSupplierWords = (): string[] => {
    if (!suppliers) return []
    const words: string[] = []
    for (const supplier of suppliers) {
      // Add each word from supplier name
      if (supplier.name) {
        words.push(...supplier.name.toLowerCase().split(/\s+/))
      }
      // Add each alias and its words
      if (supplier.aliases) {
        for (const alias of supplier.aliases) {
          if (alias) {
            words.push(...alias.toLowerCase().split(/\s+/))
          }
        }
      }
    }
    // Remove duplicates and filter short words
    return [...new Set(words)].filter(w => w.length > 1)
  }

  // Extract meaningful keywords from description for search
  const extractKeywords = (description: string): string => {
    if (!description) return ''
    let text = description
    // Remove pack sizes (12x1L, 120x15g)
    text = text.replace(/\b\d+\s*x\s*\d+(\.\d+)?\s*(g|kg|ml|ltr|l|oz|cl)?\b/gi, '')
    // Remove quantity patterns
    text = text.replace(/\b(qty|quantity)\s*:?\s*\d+\b/gi, '')
    text = text.replace(/\bcase\s*(of\s*)?\d+\b/gi, '')
    // Remove product codes like (L-AG)
    text = text.replace(/\([A-Z]{1,3}-?[A-Z0-9]{1,5}\)/gi, '')
    text = text.replace(/\[[A-Z0-9-]+\]/gi, '')
    // Remove generic packaging terms
    text = text.replace(/\b(case|qty|un|pack|box|each|per|unit|pkt|bag|bottle|tin|can|carton|tray|portion|portions)\b/gi, '')
    // Remove standalone numbers and weights
    text = text.replace(/\b\d+(\.\d+)?\s*(g|kg|ml|ltr|l|oz|cl|lb)?\b/gi, '')
    // Clean up special characters
    text = text.replace(/[^\w\s]/g, ' ')
    // Remove common English stop words
    text = text.replace(/\b(the|a|an|in|on|at|by|for|with|to|of|and|or|is|it|as|be|are|was|been|being|have|has|had|do|does|did|will|would|could|should|may|might|this|that|these|those|from|into|through|during|before|after|above|below|between|under|over)\b/gi, '')
    // Remove supplier names dynamically from loaded suppliers
    const supplierWords = getSupplierWords()
    if (supplierWords.length > 0) {
      const supplierPattern = new RegExp(`\\b(${supplierWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\b`, 'gi')
      text = text.replace(supplierPattern, '')
    }
    return text.split(/\s+/).filter(w => w.length > 1).join(' ').trim()
  }

  const openSearchModal = (item: LineItem) => {
    setSearchingLineItem(item)
    const keywords = extractKeywords(item.description || '')
    setSearchQuery(keywords)
    setSearchResults(null)
    setShowSearchModal(true)
    // Auto-search if we have keywords
    if (keywords) {
      performSearch(keywords)
    }
  }

  const openPriceHistoryModal = (item: LineItem) => {
    if (!invoice?.supplier_id) return
    setHistoryModal({
      isOpen: true,
      productCode: item.product_code,
      description: item.description,
      unit: item.unit,
      supplierId: invoice.supplier_id,
      supplierName: invoice.supplier_name || 'Unknown',
      currentPrice: item.unit_price || undefined,
      sourceInvoiceId: invoice.id,
      sourceLineItemId: item.id,
    })
  }

  const performSearch = async (query: string) => {
    if (!query || query.length < 2) return
    setSearchLoading(true)
    try {
      const res = await fetch('/kitchen/api/invoices/line-items/search', {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query,
          exclude_invoice_id: id ? parseInt(id) : null
        })
      })
      if (res.ok) {
        setSearchResults(await res.json())
      }
    } finally {
      setSearchLoading(false)
    }
  }

  const handleBulkStockUpdate = async (markAsStock: boolean) => {
    if (!lineItems) return

    try {
      // Update all line items
      const promises = lineItems.map(item =>
        fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}`, {
          method: 'PATCH',
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ is_non_stock: !markAsStock }),
        })
      )

      await Promise.all(promises)
      await refetchLineItems()
      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
      setShowBulkStockModal(false)
    } catch (error) {
      console.error('Failed to bulk update stock status:', error)
    }
  }

  // LLM FEATURE — AI Match handler for ingredient matching — see LLM-MANIFEST.md for removal instructions
  const handleAiMatch = async (description: string) => {
    setAiMatchLoading(true)
    setAiMatchResults([])
    try {
      const res = await fetch(`/kitchen/api/ingredients/ai-match?description=${encodeURIComponent(description)}`, {
        credentials: 'include',
      })
      if (res.ok) {
        const data = await res.json()
        if (data.ranked?.length > 0) {
          setAiMatchResults(data.ranked)
        }
      }
    } catch { /* ignore */ }
    setAiMatchLoading(false)
  }

  // LLM FEATURE — AI Assist handler — see LLM-MANIFEST.md for removal instructions
  const handleAiAssist = async () => {
    setAiAssistLoading(true)
    setAiAssistError(null)
    setAiAssistSuggestions(null)
    setAiReconciliationMatches([])
    setAiDismissedCorrections(new Set())

    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/ai-assist`, {
        method: 'POST',
        credentials: 'include',
      })
      if (!res.ok) {
        let detail = 'AI analysis failed'
        try { const err = await res.json(); detail = err.detail || detail } catch { /* non-JSON response */ }
        throw new Error(detail)
      }
      const data = await res.json()
      if (data.suggestions) setAiAssistSuggestions(data.suggestions)
      if (data.reconciliation_matches) setAiReconciliationMatches(data.reconciliation_matches)
      if (data.error) setAiAssistError(data.error)
    } catch (error) {
      setAiAssistError(error instanceof Error ? error.message : 'AI analysis failed')
    } finally {
      setAiAssistLoading(false)
    }
  }

  const handleApplyAiCorrection = async (idx: number, field: string, value: number | string) => {
    if (!lineItems) return
    const item = lineItems[idx]
    if (!item) return
    try {
      await updateLineItemMutation.mutateAsync({ itemId: item.id, data: { [field]: value } as Partial<LineItem> })
      setAiDismissedCorrections(prev => new Set([...prev, `${idx}-${field}`]))
    } catch (error) {
      console.error('Failed to apply AI correction:', error)
    }
  }

  const handleApplyAiSupplier = async (supplierId: number) => {
    try {
      await updateMutation.mutateAsync({ supplier_id: supplierId } as Partial<Invoice>)
    } catch (error) {
      console.error('Failed to apply AI supplier match:', error)
    }
  }
  // END LLM FEATURE

  const toggleCostBreakdown = async (item: LineItem) => {
    // Open ingredient mapping modal
    setIngredientModalItem(item)
    setAiPackSizeLoading(false)
    setAiPackSizeSource(null)

    setCostBreakdownEdits({
      pack_quantity: item.pack_quantity,
      unit_size: item.unit_size,
      unit_size_type: item.unit_size_type,
      portions_per_unit: item.portions_per_unit,
      unit_price: item.unit_price,
    })
    setPortionDescription('')
    setSaveAsDefault(false)
    setCurrentDefinition(null)
    setShowLegacyPortioning(false)
    setShowCreateIngredient(false)
    setIngredientSearch('')
    setIngredientSuggestions([])
    setSelectedIngredientId(item.ingredient_id || null)
    setSelectedIngredientName(item.ingredient_name || '')
    setSelectedIngredientUnit(item.ingredient_unit || '')

    // Show conversion display immediately if pack data exists
    // Works with or without ingredient — falls back to unit_size_type as target unit
    if (item.unit_size && item.unit_size_type) {
      updateConversionDisplay(item.ingredient_unit || undefined, {
        pack_quantity: item.pack_quantity,
        unit_size: item.unit_size,
        unit_size_type: item.unit_size_type,
        unit_price: item.unit_price,
      })
    }

    // Fetch saved definition
    setDefinitionLoading(true)
    try {
      const res = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/definition`, {
        credentials: 'include',
      })
      if (res.ok) {
        const def = await res.json()
        setCurrentDefinition(def)
        setPortionDescription(def?.portion_description || '')
      }
    } catch { /* ignore */ }
    setDefinitionLoading(false)

    // Auto-suggest ingredient match from description
    if (item.description && !item.ingredient_id) {
      searchIngredients(item.description)
    }

    // LLM FEATURE — see LLM-MANIFEST.md for removal instructions
    // Auto-deduce pack size if not already set (regex → unit field → AI, all server-side)
    if (!item.pack_quantity && !item.unit_size) {
      setAiPackSizeLoading(true)
      try {
        const packRes = await fetch(`/kitchen/api/invoices/${id}/line-items/${item.id}/ai-pack-size`, {
          credentials: 'include',
        })
        if (packRes.ok) {
          const packData = await packRes.json()
          if (packData.pack_quantity && packData.unit_size) {
            setAiPackSizeSource(packData.source)
            setCostBreakdownEdits(prev => ({
              ...prev,
              pack_quantity: packData.pack_quantity,
              unit_size: packData.unit_size,
              unit_size_type: packData.unit_size_type,
            }))
            updateConversionDisplay(item.ingredient_unit || undefined, {
              pack_quantity: packData.pack_quantity,
              unit_size: packData.unit_size,
              unit_size_type: packData.unit_size_type,
              unit_price: item.unit_price,
            })
          }
        }
      } catch { /* non-blocking */ }
      setAiPackSizeLoading(false)
    }
    // END LLM FEATURE
  }

  const searchIngredients = async (query: string) => {
    if (!query || query.length < 2) {
      setIngredientSuggestions([])
      return
    }
    setIngredientSearchLoading(true)
    try {
      const res = await fetch(`/kitchen/api/ingredients/suggest?description=${encodeURIComponent(query)}`, {
        credentials: 'include',
      })
      if (res.ok) {
        const data = await res.json()
        setIngredientSuggestions(data)
      }
    } catch { /* ignore */ }
    setIngredientSearchLoading(false)
  }

  const selectIngredient = (ing: IngredientSuggestion) => {
    setSelectedIngredientId(ing.id)
    setSelectedIngredientName(ing.name)
    setSelectedIngredientUnit(ing.standard_unit)
    setIngredientSearch('')
    setIngredientSuggestions([])
    // Default the unit type to ingredient's standard unit if not already set
    if (!costBreakdownEdits.unit_size_type) {
      setCostBreakdownEdits(prev => ({ ...prev, unit_size_type: ing.standard_unit }))
    }
    updateConversionDisplay(ing.standard_unit)
  }

  const updateConversionDisplay = (stdUnit?: string, overrides?: Partial<LineItem>) => {
    const edits = overrides ? { ...costBreakdownEdits, ...overrides } : costBreakdownEdits
    const unit = stdUnit || selectedIngredientUnit || edits.unit_size_type
    const pq = edits.pack_quantity || 1
    const us = edits.unit_size
    const ust = edits.unit_size_type
    const up = edits.unit_price
    if (!us || !ust || !unit) {
      setIngredientConversionDisplay('')
      return
    }
    // Unit conversion factors: source unit → ingredient standard unit
    const conversions: Record<string, Record<string, number>> = {
      g: { g: 1, kg: 0.001 }, kg: { g: 1000, kg: 1 }, oz: { g: 28.3495, kg: 0.0283495 },
      ml: { ml: 1, ltr: 0.001 }, cl: { ml: 10, ltr: 0.01 }, ltr: { ml: 1000, ltr: 1 },
      each: { each: 1 },
    }
    const conv = conversions[ust]?.[unit]
    if (!conv) {
      setIngredientConversionDisplay(ust !== unit ? `Cannot convert ${ust} → ${unit}` : `${us}${ust}`)
      return
    }
    const totalStd = pq * us * conv
    const pricePerStd = up ? (up / totalStd) : null
    const packNote = pq > 1 ? `${pq} × ${us}${ust} = ` : ''
    // Build display string
    let display = `${packNote}${totalStd.toFixed(totalStd % 1 ? 2 : 0)} ${unit}`
    if (pricePerStd) {
      display += ` → £${pricePerStd.toFixed(4)} per ${unit}`
      // For g/ml also show per kg/ltr
      if (unit === 'g' && pricePerStd) {
        display += ` (£${(pricePerStd * 1000).toFixed(2)}/kg)`
      } else if (unit === 'ml' && pricePerStd) {
        display += ` (£${(pricePerStd * 1000).toFixed(2)}/ltr)`
      }
    }
    setIngredientConversionDisplay(display)
  }

  const handleIngredientCreated = (_result: IngredientModalResult) => {
    // IngredientModal already created the source mapping, so close everything
    refetchLineItems()
    closeIngredientModal()
  }

  const closeIngredientModal = () => {
    setIngredientModalItem(null)
    setCostBreakdownEdits({})
    setPortionDescription('')
    setSaveAsDefault(false)
    setCurrentDefinition(null)
    setSelectedIngredientId(null)
    setSelectedIngredientName('')
    setSelectedIngredientUnit('')
    setShowCreateIngredient(false)
    setIngredientSearch('')
    setIngredientSuggestions([])
    setIngredientConversionDisplay('')
  }

  const saveCostBreakdown = async (itemId: number) => {
    // Update the line item pack fields
    await updateLineItemMutation.mutateAsync({ itemId, data: costBreakdownEdits })

    // If an ingredient is selected, set ingredient_id and create/update ingredient_source
    if (selectedIngredientId) {
      try {
        // Set ingredient_id on line item
        await fetch(`/kitchen/api/invoices/${id}/line-items/${itemId}`, {
          method: 'PATCH',
          credentials: 'include', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ ingredient_id: selectedIngredientId }),
        })

        // Create ingredient source mapping (links supplier product → ingredient)
        if (invoice?.supplier_id) {
          const sourceData: Record<string, unknown> = {
            supplier_id: invoice.supplier_id,
            pack_quantity: costBreakdownEdits.pack_quantity || 1,
            unit_size: costBreakdownEdits.unit_size || null,
            unit_size_type: costBreakdownEdits.unit_size_type || selectedIngredientUnit || null,
          }
          // Use product_code if available, otherwise description pattern
          const li = ingredientModalItem
          if (li?.product_code) {
            sourceData.product_code = li.product_code
          } else if (li?.description) {
            sourceData.description_pattern = li.description.substring(0, 100).toLowerCase().trim()
          }
          // Include price and invoice ref for price tracking
          if (costBreakdownEdits.unit_price) {
            sourceData.latest_unit_price = costBreakdownEdits.unit_price
          }
          if (id) {
            sourceData.invoice_id = parseInt(id as string)
          }
          const srcRes = await fetch(`/kitchen/api/ingredients/${selectedIngredientId}/sources`, {
            method: 'POST',
            credentials: 'include', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(sourceData),
          })
          if (!srcRes.ok) {
            const errBody = await srcRes.json().catch(() => ({}))
            console.warn('Source creation failed:', srcRes.status, errBody)
          }
        }
      } catch (err) {
        console.error('Failed to set ingredient mapping:', err)
      }
    }

    // Save as default definition if checked
    if (saveAsDefault) {
      try {
        await saveDefinitionMutation.mutateAsync({ itemId, portionDesc: portionDescription })
      } catch (err) {
        console.error('Failed to save definition:', err)
      }
    }

    closeIngredientModal()
    refetchLineItems()
  }

  // Sort and filter line items - must be before early returns (Rules of Hooks)
  const filteredAndSortedLineItems = useMemo(() => {
    if (!lineItems) return []

    // Use line_number (original OCR index) so bounding boxes stay correct after deletions
    let filtered = lineItems.map((item) => ({ ...item, _originalIndex: item.line_number }))

    // Filter by search text (product code or description)
    if (lineItemSearchText) {
      const searchLower = lineItemSearchText.toLowerCase()
      filtered = filtered.filter(item => {
        const code = (item.product_code || '').toLowerCase()
        const desc = (item.description || '').toLowerCase()
        return code.includes(searchLower) || desc.includes(searchLower)
      })
    }

    // Filter by price change status
    if (lineItemPriceFilter) {
      filtered = filtered.filter(item => {
        if (lineItemPriceFilter === 'consistent') return item.price_change_status === 'consistent'
        if (lineItemPriceFilter === 'amber') return item.price_change_status === 'amber'
        if (lineItemPriceFilter === 'red') return item.price_change_status === 'red'
        if (lineItemPriceFilter === 'no_history') return item.price_change_status === 'no_history'
        return true
      })
    }

    // Filter by portions definition
    if (lineItemPortionsFilter) {
      filtered = filtered.filter(item => {
        const hasPortions = item.portions_per_unit != null && item.portions_per_unit > 0
        if (lineItemPortionsFilter === 'yes') return hasPortions
        if (lineItemPortionsFilter === 'no') return !hasPortions
        return true
      })
    }

    // Filter by missing key data
    if (lineItemMissingDataFilter === 'missing') {
      filtered = filtered.filter(item => {
        const missingQty = item.quantity == null || item.quantity === 0
        const missingPrice = item.unit_price == null || item.unit_price === 0
        const missingAmount = item.amount == null || item.amount === 0
        return missingQty || missingPrice || missingAmount
      })
    }

    // Sort
    if (lineItemSortColumn) {
      filtered.sort((a, b) => {
        let aVal: any = null
        let bVal: any = null

        switch (lineItemSortColumn) {
          case 'code':
            aVal = a.product_code || ''
            bVal = b.product_code || ''
            break
          case 'description':
            aVal = a.description || ''
            bVal = b.description || ''
            break
          case 'unit':
            aVal = a.unit || ''
            bVal = b.unit || ''
            break
          case 'quantity':
            aVal = a.quantity || 0
            bVal = b.quantity || 0
            break
          case 'unit_price':
            aVal = a.unit_price || 0
            bVal = b.unit_price || 0
            break
          case 'price_change':
            aVal = a.price_change_percent || 0
            bVal = b.price_change_percent || 0
            break
          case 'amount':
            aVal = a.amount || 0
            bVal = b.amount || 0
            break
        }

        if (typeof aVal === 'string') {
          return lineItemSortDirection === 'asc'
            ? aVal.localeCompare(bVal)
            : bVal.localeCompare(aVal)
        } else {
          return lineItemSortDirection === 'asc' ? aVal - bVal : bVal - aVal
        }
      })
    }

    return filtered
  }, [lineItems, lineItemSortColumn, lineItemSortDirection, lineItemPriceFilter, lineItemSearchText, lineItemPortionsFilter, lineItemMissingDataFilter])

  // Track visible page in line items table for sticky indicator
  useEffect(() => {
    const container = lineItemsTableRef.current
    if (!container) return

    const handleScroll = () => {
      // Find all rows with data-page attribute
      const rows = container.querySelectorAll('tr[data-page]')
      if (rows.length === 0) return

      const containerTop = container.getBoundingClientRect().top + 50 // Account for sticky header

      // Find the first visible row (closest to top of container)
      for (const row of rows) {
        const rect = row.getBoundingClientRect()
        if (rect.top >= containerTop - 10) {
          const pageNum = parseInt(row.getAttribute('data-page') || '1', 10)
          setCurrentVisiblePage(pageNum)
          return
        }
      }

      // If no row found above container top, use the last row's page
      const lastRow = rows[rows.length - 1]
      const pageNum = parseInt(lastRow.getAttribute('data-page') || '1', 10)
      setCurrentVisiblePage(pageNum)
    }

    container.addEventListener('scroll', handleScroll)
    // Initial check
    handleScroll()

    return () => container.removeEventListener('scroll', handleScroll)
  }, [filteredAndSortedLineItems])

  // Calculate line item statistics for checks section
  const lineItemStats = useMemo(() => {
    if (!lineItems) return {
      total: 0,
      withPortions: 0,
      withoutPortions: 0,
      missingData: 0,
      nonStock: 0,
      priceCalcErrors: 0,
      totalsMatch: true,
      totalDifference: 0,
      lineItemsTotal: 0,
      stockItemsTotal: 0,
      nonStockItemsTotal: 0,
      stockItemsGross: 0
    }

    const withPortions = lineItems.filter(item =>
      item.portions_per_unit != null && item.portions_per_unit > 0
    ).length

    const missingData = lineItems.filter(item => {
      const missingQty = item.quantity == null || item.quantity === 0
      const missingPrice = item.unit_price == null || item.unit_price === 0
      const missingAmount = item.amount == null || item.amount === 0
      return missingQty || missingPrice || missingAmount
    }).length

    const nonStock = lineItems.filter(item => item.is_non_stock).length

    // Count items with price calculation errors
    // Valid cases: qty only (free item), or all three values with correct calculation
    const priceCalcErrors = lineItems.filter(item => {
      const hasQty = item.quantity != null && item.quantity !== 0
      const hasPrice = item.unit_price != null && item.unit_price !== 0
      const hasTotal = item.amount != null && item.amount !== 0

      // qty * price should equal amount (works for both positive and negative/credit lines)
      const expectedTotal = (item.quantity || 0) * (item.unit_price || 0)
      const actualTotal = item.amount || 0
      const calculationMismatch = Math.abs(expectedTotal - actualTotal) > 0.02

      return (
        (hasPrice && (!hasQty || !hasTotal)) ||  // price requires both qty and total
        (hasTotal && (!hasQty || !hasPrice)) ||  // total requires both qty and price
        (hasQty && hasPrice && hasTotal && calculationMismatch) // calculation mismatch
      )
    }).length

    // Calculate totals (line items are NET values)
    const lineItemsTotal = lineItems.reduce((sum, item) => sum + (item.amount || 0), 0)
    const stockItemsTotal = lineItems
      .filter(item => !item.is_non_stock)
      .reduce((sum, item) => sum + (item.amount || 0), 0)
    const nonStockItemsTotal = lineItems
      .filter(item => item.is_non_stock)
      .reduce((sum, item) => sum + (item.amount || 0), 0)

    // Calculate VAT ratio from invoice totals
    const invoiceGross = parseFloat(total) || 0
    const invoiceNet = parseFloat(netTotal) || invoiceGross
    const vatRatio = invoiceNet > 0 ? invoiceGross / invoiceNet : 1

    // Line item amounts are NET values
    // Calculate GROSS for stock items by multiplying by VAT ratio
    const stockItemsNet = stockItemsTotal
    const stockItemsGross = stockItemsTotal * vatRatio

    // Check if line items total matches invoice total
    // Line items are NET, so compare against invoice NET (or gross if no net available)
    const compareTotal = invoiceNet
    const difference = Math.abs(compareTotal - lineItemsTotal)
    const totalsMatch = difference <= TOLERANCE

    return {
      total: lineItems.length,
      withPortions,
      withoutPortions: lineItems.length - withPortions,
      missingData,
      nonStock,
      priceCalcErrors,
      totalsMatch,
      totalDifference: difference,
      lineItemsTotal,
      stockItemsTotal,
      stockItemsNet,
      nonStockItemsTotal,
      stockItemsGross
    }
  }, [lineItems, total, netTotal])

  // Check if line items have validation errors (for disabling Confirm/Dext buttons)
  const hasLineItemErrors = useMemo(() => {
    // No line items = error
    if (!lineItems || lineItems.length === 0) return true

    // Totals don't match = error
    if (!lineItemStats.totalsMatch) return true

    // Price calculation errors = error
    if (lineItemStats.priceCalcErrors > 0) return true

    return false
  }, [lineItems, lineItemStats.totalsMatch, lineItemStats.priceCalcErrors])

  // Calculate filter option counts for disabling
  const filterOptionCounts = useMemo(() => {
    if (!lineItems) return {
      consistent: 0,
      amber: 0,
      red: 0,
      no_history: 0,
      withPortions: 0,
      withoutPortions: 0,
      missingData: 0
    }

    return {
      consistent: lineItems.filter(item => item.price_change_status === 'consistent').length,
      amber: lineItems.filter(item => item.price_change_status === 'amber').length,
      red: lineItems.filter(item => item.price_change_status === 'red').length,
      no_history: lineItems.filter(item => item.price_change_status === 'no_history').length,
      withPortions: lineItems.filter(item => item.portions_per_unit != null && item.portions_per_unit > 0).length,
      withoutPortions: lineItems.filter(item => !(item.portions_per_unit != null && item.portions_per_unit > 0)).length,
      missingData: lineItems.filter(item => {
        const missingQty = item.quantity == null || item.quantity === 0
        const missingPrice = item.unit_price == null || item.unit_price === 0
        const missingAmount = item.amount == null || item.amount === 0
        return missingQty || missingPrice || missingAmount
      }).length
    }
  }, [lineItems])

  const handleLineItemSort = (column: string) => {
    if (lineItemSortColumn === column) {
      setLineItemSortDirection(lineItemSortDirection === 'asc' ? 'desc' : 'asc')
    } else {
      setLineItemSortColumn(column)
      setLineItemSortDirection('asc')
    }
  }

  if (isLoading) {
    return <div style={styles.loading}>Loading invoice...</div>
  }

  if (!invoice) {
    return <div style={styles.error}>Invoice not found</div>
  }

  const confidence = invoice.ocr_confidence
    ? (Number(invoice.ocr_confidence) * 100).toFixed(0)
    : null

  // Check if the file is a PDF
  const isPDF = invoice?.image_path?.toLowerCase().endsWith('.pdf')

  return (
    <div style={styles.pageContainer}>
      {/* Error banner for Azure OCR failures */}
      {invoice?.ocr_raw_text && invoice.ocr_raw_text.startsWith('Error:') && (
        <div style={styles.errorBanner}>
          <div style={styles.errorBannerIcon}>⚠️</div>
          <div style={styles.errorBannerContent}>
            <strong>OCR Processing Error</strong>
            <p>{invoice.ocr_raw_text.replace('Error: ', '')}</p>
            {invoice.ocr_raw_text.includes('quota exceeded') && (
              <p style={{ marginTop: '0.5rem', fontSize: '0.9rem' }}>
                <strong>Next steps:</strong> Check your Azure subscription budget limits in the Azure portal.
                Once resolved, use the "Reprocess" button below to retry OCR extraction.
              </p>
            )}
            {invoice.ocr_raw_text.includes('authentication failed') && (
              <p style={{ marginTop: '0.5rem', fontSize: '0.9rem' }}>
                <strong>Next steps:</strong> Verify your Azure API credentials in Settings → Azure Configuration.
              </p>
            )}
          </div>
        </div>
      )}

      {/* Processing banner for PENDING invoices */}
      {invoice.status === 'PENDING' && (
        <div style={{
          display: 'flex',
          gap: '1rem',
          padding: '1rem 1.5rem',
          marginBottom: '1.5rem',
          background: '#d4edda',
          border: '2px solid #28a745',
          borderRadius: '8px',
          color: '#155724',
          alignItems: 'center',
        }}>
          <div style={{ fontSize: '1.5rem', flexShrink: 0 }}>&#9881;</div>
          <div>
            <strong>Processing Invoice</strong>
            <p style={{ margin: '0.25rem 0 0' }}>Azure OCR extraction is in progress. The page will reload automatically when complete.</p>
          </div>
        </div>
      )}

      {/* Top row: Image and Form side by side */}
      <div style={styles.topRow}>
        <div style={styles.imageSection} ref={containerRef}>
          <h3>Invoice {isPDF ? 'Document' : 'Image'}</h3>
          {imageUrl ? (
            isPDF ? (
              <div style={styles.pdfScrollContainer} ref={pdfContainerRef}>
                {pdfPages.length > 0 ? (
                  pdfPages.map((page, pageIndex) => {
                    const pageNum = pageIndex + 1
                    const fieldBbox = highlightedField ? getFieldBoundingBox(highlightedField) : null
                    const lineItemBbox = expandedLineItem !== null && lineItems
                      ? (() => {
                          const item = lineItems.find(item => item.id === expandedLineItem)
                          return item ? getLineItemBoundingBox(item.line_number) : null
                        })()
                      : null

                    return (
                      <div key={pageIndex} style={styles.pdfPageWrapper}>
                        {/* Wrapper for image + highlights with actual size scaling for scrollable zoom */}
                        <div
                          data-page-container
                          style={{
                            ...styles.pdfPageContainer,
                            position: 'relative',
                            width: page.displayWidth * (zoomPageNum === pageNum ? zoomLevel : 1),
                            height: page.displayHeight * (zoomPageNum === pageNum ? zoomLevel : 1),
                            margin: '0 auto',
                          }}>
                          <img
                            src={page.canvas.toDataURL()}
                            alt={`Page ${pageNum}`}
                            style={{
                              width: '100%',
                              height: '100%',
                              display: 'block',
                            }}
                          />
                          {/* Highlight for header fields on this page */}
                          {fieldBbox && fieldBbox.pageNumber === pageNum && (() => {
                            // Add padding (0.5% ≈ 5px at typical sizes) and scale border inversely to zoom
                            const padding = 0.5
                            const borderWidth = Math.max(1, 2 / zoomLevel)
                            return (
                              <div
                                ref={highlightRef}
                                style={{
                                  ...styles.highlightOverlay,
                                  left: `${fieldBbox.x - padding}%`,
                                  top: `${fieldBbox.y - padding}%`,
                                  width: `${fieldBbox.width + padding * 2}%`,
                                  height: `${fieldBbox.height + padding * 2}%`,
                                  border: `${borderWidth}px solid #ffc107`,
                                }}
                                onClick={() => { setHighlightedField(null); resetZoom(); }}
                              />
                            )
                          })()}
                          {/* Highlight for line items on this page */}
                          {lineItemBbox && lineItemBbox.pageNumber === pageNum && (() => {
                            const padding = 0.5
                            const borderWidth = Math.max(1, 2 / zoomLevel)
                            return (
                              <div
                                style={{
                                  ...styles.highlightOverlay,
                                  left: `${lineItemBbox.x - padding}%`,
                                  top: `${lineItemBbox.y - padding}%`,
                                  width: `${lineItemBbox.width + padding * 2}%`,
                                  height: `${lineItemBbox.height + padding * 2}%`,
                                  border: `${borderWidth}px solid #ffc107`,
                                }}
                                onClick={() => { setExpandedLineItem(null); resetZoom(); }}
                              />
                            )
                          })()}
                        </div>
                        {pdfPages.length > 1 && (
                          <div style={styles.pageLabel}>Page {pageNum} of {pdfPages.length}</div>
                        )}
                      </div>
                    )
                  })
                ) : (
                  <div style={styles.imagePlaceholder}>Rendering PDF...</div>
                )}
              </div>
            ) : (
              <>
                <div style={styles.imageZoomControls}>
                  <button onClick={() => setImageZoom(Math.min(imageZoom + 0.5, 5))} style={styles.zoomBtn} title="Zoom In">
                    +
                  </button>
                  <span style={styles.zoomLabel}>{Math.round(imageZoom * 100)}%</span>
                  <button onClick={() => setImageZoom(Math.max(imageZoom - 0.5, 1))} style={styles.zoomBtn} title="Zoom Out">
                    −
                  </button>
                  {imageZoom > 1 && (
                    <button onClick={() => setImageZoom(1)} style={styles.zoomResetBtnSmall} title="Reset Zoom">
                      Reset
                    </button>
                  )}
                </div>
                <div
                  style={{
                    ...styles.imageScrollContainer,
                    cursor: imageZoom > 1 ? 'grab' : 'default'
                  }}
                  ref={imageContainerRef}
                  onWheel={(e) => {
                    if (e.ctrlKey || e.metaKey) {
                      e.preventDefault()
                      const delta = e.deltaY > 0 ? -0.1 : 0.1
                      setImageZoom(Math.max(1, Math.min(5, imageZoom + delta)))
                    }
                  }}
                >
                  <div style={{
                    ...styles.imageContainer,
                    width: `${imageZoom * 100}%`,
                    height: 'auto'
                  }}>
                    <img
                      src={imageUrl}
                      alt="Invoice"
                      style={{
                        ...styles.image,
                        width: '100%',
                        height: 'auto'
                      }}
                    />
                    {highlightedField && getFieldBoundingBox(highlightedField) && (() => {
                      const bbox = getFieldBoundingBox(highlightedField)!
                      const padding = 0.5
                      return (
                        <div
                          style={{
                            ...styles.highlightOverlay,
                            left: `${bbox.x - padding}%`,
                            top: `${bbox.y - padding}%`,
                            width: `${bbox.width + padding * 2}%`,
                            height: `${bbox.height + padding * 2}%`,
                            border: '2px solid #ffc107',
                          }}
                          onClick={() => setHighlightedField(null)}
                        />
                      )
                    })()}
                  </div>
                </div>
              </>
            )
          ) : (
            <div style={styles.imagePlaceholder}>Loading {isPDF ? 'document' : 'image'}...</div>
          )}
          {isPDF && zoomLevel > 1 && (
            <button
              onClick={resetZoom}
              style={styles.zoomResetBtn}
            >
              Reset Zoom ({zoomLevel}x)
            </button>
          )}
          {isPDF && imageUrl && (
            <a
              href={`/kitchen/api/invoices/${id}/file`}
              target="_blank"
              rel="noopener noreferrer"
              style={styles.openPdfLink}
            >
              Open PDF in new tab
            </a>
          )}
          {confidence && (
            <div style={styles.confidenceBadge}>
              OCR Confidence: {confidence}%
            </div>
          )}
        </div>

        <div style={styles.formSection}>
          {/* Duplicate Warning Banner */}
          {invoice.duplicate_status && (
            <div
              style={{
                ...styles.duplicateWarning,
                background: invoice.duplicate_status === 'firm_duplicate' ? '#f8d7da' : '#fff3cd',
                borderColor: invoice.duplicate_status === 'firm_duplicate' ? '#f5c6cb' : '#ffeeba',
                color: invoice.duplicate_status === 'firm_duplicate' ? '#721c24' : '#856404',
              }}
              onClick={() => setShowDuplicateModal(true)}
            >
              {invoice.duplicate_status === 'firm_duplicate'
                ? '⚠️ DUPLICATE: This invoice matches an existing record. Click to compare.'
                : '⚠️ Possible duplicate detected. Click to compare.'}
            </div>
          )}

          {/* PO Linked Indicator */}
          {poMatchData?.linked_po && (
            <div style={{
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'space-between',
              padding: '0.75rem 1rem',
              marginBottom: '1rem',
              background: '#d4edda',
              border: '1px solid #c3e6cb',
              borderRadius: '8px',
              color: '#155724',
              fontSize: '0.9rem',
            }}>
              <div>
                <strong>Linked to PO-{poMatchData.linked_po.po_id}</strong>
                {' '}({poMatchData.linked_po.total_amount != null ? `£${poMatchData.linked_po.total_amount.toFixed(2)}` : 'N/A'}, {poMatchData.linked_po.order_date ? new Date(poMatchData.linked_po.order_date + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : ''})
              </div>
              <div style={{ display: 'flex', gap: '0.5rem' }}>
                <button
                  onClick={() => setViewingPoId(poMatchData.linked_po!.po_id)}
                  style={{ padding: '0.3rem 0.6rem', border: '1px solid #155724', borderRadius: '4px', background: 'white', color: '#155724', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600 }}
                >
                  View PO
                </button>
                <button
                  onClick={() => handleUnlinkPo(poMatchData.linked_po!.po_id)}
                  style={{ padding: '0.3rem 0.6rem', border: '1px solid #856404', borderRadius: '4px', background: '#fff3cd', color: '#856404', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600 }}
                >
                  Unlink
                </button>
              </div>
            </div>
          )}

          {/* PO Matching Suggestions */}
          {!poMatchData?.linked_po && poMatchData?.matches && poMatchData.matches.length > 0 && (
            <div style={{
              padding: '0.75rem 1rem',
              marginBottom: '1rem',
              background: '#e3f2fd',
              border: '1px solid #bbdefb',
              borderRadius: '8px',
              fontSize: '0.9rem',
              color: '#1565c0',
            }}>
              <div style={{ fontWeight: 600, marginBottom: '0.5rem' }}>
                This supplier has {poMatchData.matches.length} pending Purchase Order{poMatchData.matches.length > 1 ? 's' : ''}
              </div>
              {poMatchData.matches.map((m, idx) => {
                const isSuggested = idx === 0 && m.confidence >= 0.8
                const isPotential = !isSuggested && m.confidence >= 0.8
                return (
                <div key={m.po_id} style={{
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'space-between',
                  padding: '0.4rem 0.5rem',
                  marginBottom: '0.25rem',
                  background: 'rgba(255,255,255,0.6)',
                  borderRadius: '4px',
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                    <span style={{ fontWeight: 500 }}>PO-{m.po_id}:</span>
                    <span>{m.total_amount != null ? `£${m.total_amount.toFixed(2)}` : 'N/A'}</span>
                    <span style={{ color: '#666' }}>
                      ({m.order_date ? new Date(m.order_date + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) : 'No date'})
                    </span>
                    {isSuggested && (
                      <span style={{
                        background: '#4caf50',
                        color: 'white',
                        padding: '0.1rem 0.4rem',
                        borderRadius: '3px',
                        fontSize: '0.75rem',
                        fontWeight: 600,
                      }}>
                        Suggested match
                      </span>
                    )}
                    {isPotential && (
                      <span style={{
                        background: '#ff9800',
                        color: 'white',
                        padding: '0.1rem 0.4rem',
                        borderRadius: '3px',
                        fontSize: '0.75rem',
                        fontWeight: 600,
                      }}>
                        Potential match
                      </span>
                    )}
                  </div>
                  <div style={{ display: 'flex', gap: '0.4rem' }}>
                    <button
                      onClick={() => setViewingPoId(m.po_id)}
                      style={{ padding: '0.2rem 0.5rem', border: '1px solid #1565c0', borderRadius: '4px', background: 'white', color: '#1565c0', cursor: 'pointer', fontSize: '0.8rem' }}
                    >
                      View
                    </button>
                    <button
                      onClick={() => handleLinkPo(m.po_id)}
                      style={{ padding: '0.2rem 0.5rem', border: 'none', borderRadius: '4px', background: '#1565c0', color: 'white', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600 }}
                    >
                      Link
                    </button>
                  </div>
                </div>
                )
              })}
            </div>
          )}

          {/* Header: Title | Source Badge | Status Badge */}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
            <h3 style={{ margin: 0 }}>Invoice Details</h3>
            <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
              {invoice.source && invoice.source !== 'upload' && (
                <SourceBadge source={invoice.source} sourceReference={invoice.source_reference} />
              )}
              <span style={{
                padding: '0.4rem 0.8rem',
                borderRadius: '4px',
                fontSize: '0.85rem',
                fontWeight: '600',
                background: invoice.status === 'CONFIRMED' ? '#22c55e' : invoice.status === 'REVIEWED' ? '#8b5cf6' : invoice.status === 'PROCESSED' ? '#3b82f6' : '#f59e0b',
                color: 'white'
              }}>
                {invoice.status.toUpperCase()}
                {invoice.document_type === 'delivery_note' && ' (DN)'}
              </span>
            </div>
          </div>

          <div style={styles.form}>
            {/* Supplier (full width) */}
            <div style={styles.label}>
              <span>Supplier</span>
              <div style={styles.supplierRow}>
                <select
                  value={supplierId}
                  onChange={(e) => setSupplierId(e.target.value)}
                  style={{
                    ...styles.input,
                    flex: 1,
                    ...(invoice?.supplier_match_type === 'fuzzy' && supplierId ? {
                      border: '2px solid #f0ad4e',
                      background: '#fff8e6'
                    } : {})
                  }}
                >
                  <option value="">-- Select Supplier --</option>
                  {suppliers?.map((s) => (
                    <option key={s.id} value={s.id}>{s.name}</option>
                  ))}
                </select>
                {getFieldBoundingBox('VendorName') && (
                  <button
                    type="button"
                    onClick={() => handleHighlightField('VendorName')}
                    style={{
                      ...styles.eyeBtn,
                      ...(highlightedField === 'VendorName' ? styles.eyeBtnActive : {})
                    }}
                    title="Show on document"
                  >
                    👁
                  </button>
                )}
                <button
                  type="button"
                  onClick={openCreateSupplierModal}
                  style={styles.createSupplierBtn}
                  title="Create new supplier"
                >
                  + New
                </button>
              </div>
              {invoice?.supplier_match_type === 'fuzzy' && supplierId && invoice.vendor_name && (
                <div style={styles.fuzzyMatchWarning}>
                  <span>Fuzzy match from "{invoice.vendor_name}" - please verify</span>
                  <button
                    type="button"
                    onClick={() => addAliasMutation.mutate({
                      supplierId: parseInt(supplierId),
                      alias: invoice.vendor_name!,
                      invoiceId: invoice.id
                    })}
                    style={styles.addAliasBtn}
                    disabled={addAliasMutation.isPending}
                    title="Add this name as an alias so future invoices match exactly"
                  >
                    {addAliasMutation.isPending ? 'Adding...' : '+ Add as alias'}
                  </button>
                </div>
              )}
              {invoice?.vendor_name && !supplierId && (
                <div style={styles.extractedHintRow}>
                  <span style={styles.extractedHint}>Extracted: {invoice.vendor_name}</span>
                  <button
                    type="button"
                    onClick={openCreateSupplierModal}
                    style={styles.createFromExtractedBtn}
                  >
                    Create "{invoice.vendor_name}"
                  </button>
                </div>
              )}
            </div>

            {/* Date | Type */}
            <div style={styles.row}>
              <label style={{ ...styles.label, flex: 1 }}>
                Invoice Date
                {(() => {
                  const dateWarning = getDateWarning(invoiceDate || null, invoice?.status || '');
                  return (
                    <>
                      <div style={styles.inputWithEye}>
                        <input
                          type="date"
                          value={invoiceDate}
                          onChange={(e) => setInvoiceDate(e.target.value)}
                          style={{ ...styles.input, ...dateWarningStyles[dateWarning] }}
                        />
                        {getFieldBoundingBox('InvoiceDate') && (
                          <button
                            type="button"
                            onClick={() => handleHighlightField('InvoiceDate')}
                            style={{
                              ...styles.eyeBtn,
                              ...(highlightedField === 'InvoiceDate' ? styles.eyeBtnActive : {})
                            }}
                            title="Show on document"
                          >
                            👁
                          </button>
                        )}
                        {invoice?.status !== 'CONFIRMED' && (
                          <button
                            type="button"
                            onClick={handleFetchParsedDates}
                            style={{
                              background: 'none',
                              border: 'none',
                              cursor: 'pointer',
                              padding: '2px 4px',
                              fontSize: '1rem',
                              color: (!invoiceDate || dateWarning !== 'none') ? '#007bff' : '#ccc',
                              display: 'flex',
                              alignItems: 'center',
                            }}
                            title="Find dates in document"
                          >
                            🔍
                          </button>
                        )}
                      </div>
                      {dateWarning !== 'none' && (
                        <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
                          {dateWarning === 'future' ? (
                            <button
                              type="button"
                              onClick={handleFetchParsedDates}
                              style={{
                                background: 'none',
                                border: 'none',
                                padding: 0,
                                cursor: 'pointer',
                                fontSize: '0.8rem',
                                color: '#e85d04',
                                fontWeight: 600,
                                textDecoration: 'underline',
                                textDecorationStyle: 'dotted',
                              }}
                              title="Click to fix date"
                            >
                              ⚠ Date is in the future — likely a day/month swap — click to fix
                            </button>
                          ) : (
                            <span style={{ fontSize: '0.8rem', color: dateWarning === 'red' ? '#dc3545' : '#856404' }}>
                              {!invoiceDate ? 'No date set' : 'Date differs from today by more than 7 days'}
                            </span>
                          )}
                        </div>
                      )}
                    </>
                  );
                })()}
              </label>

              <label style={{ ...styles.label, flex: 1 }}>
                Document Type
                <select
                  value={documentType}
                  onChange={(e) => setDocumentType(e.target.value)}
                  style={styles.input}
                >
                  <option value="invoice">Invoice</option>
                  <option value="credit_note">Credit Note</option>
                  <option value="delivery_note">Delivery Note</option>
                </select>
              </label>
            </div>

            {/* Number | PO */}
            <div style={styles.row}>
              <label style={{ ...styles.label, flex: 1 }}>
                Invoice Number
                <div style={styles.inputWithEye}>
                  <input
                    type="text"
                    value={invoiceNumber}
                    onChange={(e) => setInvoiceNumber(e.target.value)}
                    style={styles.input}
                    placeholder="e.g., INV-12345"
                  />
                  {getFieldBoundingBox('InvoiceId') && (
                    <button
                      type="button"
                      onClick={() => handleHighlightField('InvoiceId')}
                      style={{
                        ...styles.eyeBtn,
                        ...(highlightedField === 'InvoiceId' ? styles.eyeBtnActive : {})
                      }}
                      title="Show on document"
                    >
                      👁
                    </button>
                  )}
                  {invoice?.status !== 'CONFIRMED' && (
                    <button
                      type="button"
                      onClick={handleFetchInvoiceNumberCandidates}
                      style={{
                        background: 'none',
                        border: 'none',
                        cursor: 'pointer',
                        padding: '2px 4px',
                        fontSize: '1rem',
                        color: invoiceNumber ? '#ccc' : '#007bff',
                        display: 'flex',
                        alignItems: 'center',
                      }}
                      title="Search OCR for invoice number"
                    >
                      🔍
                    </button>
                  )}
                </div>
              </label>

              <label style={{ ...styles.label, flex: 1 }}>
                Order/PO Number
                <div style={styles.inputWithEye}>
                  <input
                    type="text"
                    value={orderNumber}
                    onChange={(e) => setOrderNumber(e.target.value)}
                    style={styles.input}
                    placeholder="e.g., PO-12345"
                  />
                  {getFieldBoundingBox('PurchaseOrder') && (
                    <button
                      type="button"
                      onClick={() => handleHighlightField('PurchaseOrder')}
                      style={{
                        ...styles.eyeBtn,
                        ...(highlightedField === 'PurchaseOrder' ? styles.eyeBtnActive : {})
                      }}
                      title="Show on document"
                    >
                      👁
                    </button>
                  )}
                </div>
              </label>
            </div>

            {/* Net | Gross */}
            <div style={styles.row}>
              <label style={{ ...styles.label, flex: 1 }}>
                Net Total (£)
                <div style={styles.inputWithEye}>
                  <input
                    type="number"
                    step="0.01"
                    value={netTotal}
                    onChange={(e) => setNetTotal(e.target.value)}
                    style={styles.input}
                    placeholder="Exc. VAT"
                  />
                  {getFieldBoundingBox('SubTotal') && (
                    <button
                      type="button"
                      onClick={() => handleHighlightField('SubTotal')}
                      style={{
                        ...styles.eyeBtn,
                        ...(highlightedField === 'SubTotal' ? styles.eyeBtnActive : {})
                      }}
                      title="Show on document"
                    >
                      👁
                    </button>
                  )}
                </div>
              </label>

              <label style={{ ...styles.label, flex: 1 }}>
                Gross Total (£)
                <div style={styles.inputWithEye}>
                  <input
                    type="number"
                    step="0.01"
                    value={total}
                    onChange={(e) => setTotal(e.target.value)}
                    style={styles.input}
                    placeholder="Inc. VAT"
                  />
                  {getFieldBoundingBox('InvoiceTotal') && (
                    <button
                      type="button"
                      onClick={() => handleHighlightField('InvoiceTotal')}
                      style={{
                        ...styles.eyeBtn,
                        ...(highlightedField === 'InvoiceTotal' ? styles.eyeBtnActive : {})
                      }}
                      title="Show on document"
                    >
                      👁
                    </button>
                  )}
                </div>
              </label>
            </div>

            {/* Notes */}
            <label style={styles.label}>
              <span>
                Invoice Notes{' '}
                <small style={{ color: '#666', fontSize: '0.85em' }}>
                  (Optional, included in Dext etc.)
                </small>
              </span>
              <textarea
                value={invoiceNotes}
                onChange={(e) => setInvoiceNotes(e.target.value)}
                onBlur={async () => {
                  try {
                    await fetch(`/kitchen/api/invoices/${id}`, {
                      method: 'PATCH',
                      credentials: 'include',
                      headers: {
                        'Content-Type': 'application/json',
                      },
                      body: JSON.stringify({ notes: invoiceNotes })
                    })
                  } catch (error) {
                    console.error('Failed to save notes:', error)
                  }
                }}
                placeholder="Add notes about this invoice..."
                rows={3}
                style={{
                  ...styles.input,
                  resize: 'vertical',
                  fontFamily: 'inherit'
                }}
              />
            </label>
          </div>

          {/* Confirm & Dext (full width, auto-saves on change) */}
          <div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
            {/* Link/Resolved Dispute button - only for credit notes with a supplier */}
            {invoice?.document_type === 'credit_note' && invoice?.supplier_id && (
              invoice?.linked_dispute_id ? (
                <button
                  onClick={() => setViewingDisputeId(invoice.linked_dispute_id)}
                  style={{
                    padding: '0.75rem 1rem',
                    background: '#6c757d',
                    color: 'white',
                    border: 'none',
                    borderRadius: '8px',
                    cursor: 'pointer',
                    fontWeight: 'bold',
                    fontSize: '0.9rem',
                    whiteSpace: 'nowrap'
                  }}
                  title="View the dispute resolved by this credit note"
                >
                  ✓ Resolved Dispute
                </button>
              ) : (
                <button
                  onClick={() => setShowLinkDisputeModal(true)}
                  style={{
                    padding: '0.75rem 1rem',
                    background: '#28a745',
                    color: 'white',
                    border: 'none',
                    borderRadius: '8px',
                    cursor: 'pointer',
                    fontWeight: 'bold',
                    fontSize: '0.9rem',
                    whiteSpace: 'nowrap'
                  }}
                  title="Link this credit note to an open dispute"
                >
                  Link Dispute
                </button>
              )
            )}
            <button
              onClick={handleConfirm}
              style={{
                ...styles.confirmBtn,
                flex: 1,
                opacity: (hasLineItemErrors || updateMutation.isPending) ? 0.5 : 1,
                cursor: (hasLineItemErrors || updateMutation.isPending) ? 'not-allowed' : 'pointer'
              }}
              disabled={hasLineItemErrors || updateMutation.isPending}
              title={hasLineItemErrors ? 'Fix line item validation errors before confirming' : ''}
            >
              {invoice?.dext_sent_at || invoice?.supplier_skip_dext ? 'CONFIRM' : 'CONFIRM & DEXT'}
            </button>
          </div>

          {/* Checks Section */}
          {lineItems && lineItems.length > 0 && (
            <div style={{ margin: '20px 0', display: 'flex', gap: '8px' }}>
              {/* Items Total Check - RED if doesn't match (blocks confirm) */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `2px solid ${lineItemStats.totalsMatch ? '#c3e6cb' : '#dc2626'}`,
                background: lineItemStats.totalsMatch ? '#d4edda' : '#fef2f2',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>
                  {lineItemStats.totalsMatch ? '✓' : '⛔'}
                </div>
                <div style={{ fontWeight: '600', color: lineItemStats.totalsMatch ? '#155724' : '#dc2626', marginBottom: '2px' }}>
                  Items Total
                </div>
                <div style={{ fontSize: '0.7rem', color: lineItemStats.totalsMatch ? '#155724' : '#dc2626' }}>
                  {lineItemStats.totalsMatch ? 'Matches' : "Doesn't match"}
                </div>
              </div>

              {/* QTY/Price Check - RED if errors (blocks confirm) */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `2px solid ${lineItemStats.priceCalcErrors === 0 ? '#c3e6cb' : '#dc2626'}`,
                background: lineItemStats.priceCalcErrors === 0 ? '#d4edda' : '#fef2f2',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>
                  {lineItemStats.priceCalcErrors === 0 ? '✓' : '⛔'}
                </div>
                <div style={{ fontWeight: '600', color: lineItemStats.priceCalcErrors === 0 ? '#155724' : '#dc2626', marginBottom: '2px' }}>
                  QTY/Price
                </div>
                <div style={{ fontSize: '0.7rem', color: lineItemStats.priceCalcErrors === 0 ? '#155724' : '#dc2626' }}>
                  {lineItemStats.priceCalcErrors === 0 ? 'All OK' : `${lineItemStats.priceCalcErrors} error${lineItemStats.priceCalcErrors !== 1 ? 's' : ''}`}
                </div>
              </div>

              {/* Portions Check - AMBER warning (doesn't block) */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `1px solid ${lineItemStats.withoutPortions === 0 ? '#c3e6cb' : '#ffc107'}`,
                background: lineItemStats.withoutPortions === 0 ? '#d4edda' : '#fff3cd',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>📦</div>
                <div style={{ fontWeight: '600', color: lineItemStats.withoutPortions === 0 ? '#155724' : '#856404', marginBottom: '2px' }}>
                  Portions
                </div>
                <div style={{ fontSize: '0.7rem', color: lineItemStats.withoutPortions === 0 ? '#155724' : '#856404' }}>
                  {lineItemStats.withPortions} / {lineItemStats.total}
                </div>
              </div>

              {/* Missing Data Check - AMBER warning (doesn't block confirm) */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `1px solid ${lineItemStats.missingData === 0 ? '#c3e6cb' : '#ffc107'}`,
                background: lineItemStats.missingData === 0 ? '#d4edda' : '#fff3cd',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>
                  {lineItemStats.missingData === 0 ? '✓' : '⚠'}
                </div>
                <div style={{ fontWeight: '600', color: lineItemStats.missingData === 0 ? '#155724' : '#856404', marginBottom: '2px' }}>
                  Missing Data
                </div>
                <div style={{ fontSize: '0.7rem', color: lineItemStats.missingData === 0 ? '#155724' : '#856404' }}>
                  {lineItemStats.missingData} item{lineItemStats.missingData !== 1 ? 's' : ''}
                </div>
              </div>

              {/* Non-Stock Check */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `1px solid ${lineItemStats.nonStock === 0 ? '#c3e6cb' : '#ffc107'}`,
                background: lineItemStats.nonStock === 0 ? '#d4edda' : '#fff3cd',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>🚫</div>
                <div style={{ fontWeight: '600', color: lineItemStats.nonStock === 0 ? '#155724' : '#856404', marginBottom: '2px' }}>
                  Non-Stock
                </div>
                <div style={{ fontSize: '0.7rem', color: lineItemStats.nonStock === 0 ? '#155724' : '#856404' }}>
                  {lineItemStats.nonStock} item{lineItemStats.nonStock !== 1 ? 's' : ''}
                </div>
              </div>

              {/* Dext Status Check - Always visible */}
              <div style={{
                flex: 1,
                padding: '8px',
                borderRadius: '4px',
                border: `1px solid ${invoice.supplier_skip_dext ? '#c3e6cb' : (invoice.dext_sent_at ? '#c3e6cb' : '#ffc107')}`,
                background: invoice.supplier_skip_dext ? '#d4edda' : (invoice.dext_sent_at ? '#d4edda' : '#fff3cd'),
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                textAlign: 'center',
                fontSize: '0.8rem'
              }}>
                <div style={{ fontSize: '1.3rem', marginBottom: '2px' }}>
                  {invoice.supplier_skip_dext ? '−' : (invoice.dext_sent_at ? '✓' : '⚠')}
                </div>
                <div style={{ fontWeight: '600', color: invoice.supplier_skip_dext ? '#155724' : (invoice.dext_sent_at ? '#155724' : '#856404'), marginBottom: '2px' }}>
                  Dext Status
                </div>
                <div style={{ fontSize: '0.7rem', color: invoice.supplier_skip_dext ? '#155724' : (invoice.dext_sent_at ? '#155724' : '#856404') }}>
                  {invoice.supplier_skip_dext ? 'N/A (Skipped)' : (invoice.dext_sent_at ? new Date(invoice.dext_sent_at).toLocaleDateString() : 'Not sent')}
                </div>
              </div>
            </div>
          )}

          {/* Dispute Indicator Blocks - clickable to open detail modal */}
          {invoice.disputes && invoice.disputes.length > 0 && (
            <div style={{ marginTop: '15px', display: 'flex', flexDirection: 'column', gap: '10px' }}>
              {invoice.disputes.map((dispute) => {
                const isOpen = ['NEW', 'CONTACTED', 'AWAITING_CREDIT', 'AWAITING_REPLACEMENT'].includes(dispute.status)

                return (
                  <div
                    key={dispute.id}
                    onClick={() => setViewingDisputeId(dispute.id)}
                    style={{
                      padding: '12px',
                      background: isOpen ? '#fff3cd' : '#d1ecf1',
                      border: `2px solid ${isOpen ? '#ffc107' : '#17a2b8'}`,
                      borderRadius: '8px',
                      display: 'flex',
                      justifyContent: 'space-between',
                      alignItems: 'center',
                      cursor: 'pointer',
                    }}
                  >
                    <div>
                      <div style={{ fontWeight: 'bold', color: '#1a1a2e', marginBottom: '4px' }}>
                        {isOpen ? '⚠️' : 'ℹ️'} {dispute.status.replace(/_/g, ' ')}
                      </div>
                      <div style={{ fontSize: '0.8rem', color: '#666' }}>
                        #{dispute.id} - {dispute.dispute_type.replace(/_/g, ' ').toUpperCase()} - {dispute.title}
                      </div>
                    </div>
                    <span
                      style={{
                        padding: '6px 12px',
                        background: '#e94560',
                        color: 'white',
                        borderRadius: '4px',
                        fontSize: '0.85rem',
                        fontWeight: 'bold',
                        whiteSpace: 'nowrap'
                      }}
                    >
                      View
                    </span>
                  </div>
                )
              })}
            </div>
          )}

          {/* Button Rows - Centered */}
          <div style={{ marginTop: '15px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}>
            {/* Row 1: Resend to Dext | Delete | View OCR */}
            <div style={{ display: 'flex', gap: '10px' }}>
              {(invoice.status === 'CONFIRMED' || invoice.status === 'REVIEWED') && settings?.dext_manual_send_enabled && !invoice.supplier_skip_dext && (
                <button
                  onClick={() => setShowDextSendConfirm(true)}
                  style={{
                    padding: '0.4rem 1.2rem',
                    fontSize: '0.8rem',
                    background: '#17a2b8',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: hasLineItemErrors ? 'not-allowed' : 'pointer',
                    minWidth: '130px',
                    whiteSpace: 'nowrap',
                    opacity: hasLineItemErrors ? 0.5 : 1
                  }}
                  disabled={hasLineItemErrors}
                  title={hasLineItemErrors ? 'Fix line item validation errors before sending to Dext' : ''}
                >
                  {invoice.dext_sent_at ? 'Resend to Dext' : 'Send to Dext'}
                </button>
              )}
              <button
                onClick={() => setShowDeleteModal(true)}
                style={{
                  padding: '0.4rem 1.2rem',
                  fontSize: '0.8rem',
                  background: '#dc3545',
                  color: 'white',
                  border: 'none',
                  borderRadius: '4px',
                  cursor: 'pointer',
                  minWidth: '110px',
                  whiteSpace: 'nowrap'
                }}
              >
                Delete Invoice
              </button>
              <button
                onClick={() => setShowDisputeModal(true)}
                style={{
                  padding: '0.4rem 1.2rem',
                  fontSize: '0.8rem',
                  background: '#f0ad4e',
                  color: 'white',
                  border: 'none',
                  borderRadius: '4px',
                  cursor: 'pointer',
                  minWidth: '110px',
                  whiteSpace: 'nowrap'
                }}
              >
                Log Dispute
              </button>
              {/* LLM FEATURE — AI Assist button — see LLM-MANIFEST.md for removal instructions */}
              {settings?.llm_enabled && settings?.anthropic_api_key_set && (
                <button
                  onClick={handleAiAssist}
                  disabled={aiAssistLoading}
                  style={{
                    padding: '0.4rem 1.2rem',
                    fontSize: '0.8rem',
                    background: aiAssistSuggestions ? '#6f42c1' : '#7952b3',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: aiAssistLoading ? 'wait' : 'pointer',
                    minWidth: '120px',
                    whiteSpace: 'nowrap',
                    opacity: aiAssistLoading ? 0.7 : 1,
                  }}
                >
                  {aiAssistLoading ? 'Analysing...' : aiAssistSuggestions ? '\u2728 AI Results' : '\u2728 AI Assist'}
                </button>
              )}
              <button
                onClick={() => setShowRawOcrModal(true)}
                style={{
                  padding: '0.4rem 1.2rem',
                  fontSize: '0.8rem',
                  background: '#6c757d',
                  color: 'white',
                  border: 'none',
                  borderRadius: '4px',
                  cursor: 'pointer',
                  minWidth: '120px',
                  whiteSpace: 'nowrap'
                }}
              >
                View OCR Data
              </button>
            </div>

            {/* Row 2: Admin Only Manual Control Buttons */}
            {user?.is_admin && (
              <div style={{ display: 'flex', gap: '10px' }}>
                {!invoice.supplier_skip_dext && (
                  <button
                    onClick={handleMarkDextSent}
                    disabled={adminOperationInProgress}
                    style={{
                      padding: '0.4rem 1.2rem',
                      fontSize: '0.8rem',
                      background: adminOperationInProgress ? '#ccc' : '#17a2b8',
                      color: 'white',
                      border: 'none',
                      borderRadius: '4px',
                      cursor: adminOperationInProgress ? 'not-allowed' : 'pointer',
                      minWidth: '140px',
                      whiteSpace: 'nowrap'
                    }}
                  >
                    {adminOperationInProgress ? 'Processing...' : 'Mark Dext Sent'}
                  </button>
                )}
                <button
                  onClick={handleReprocessOCR}
                  disabled={adminOperationInProgress}
                  style={{
                    padding: '0.4rem 1.2rem',
                    fontSize: '0.8rem',
                    background: adminOperationInProgress ? '#ccc' : '#ffc107',
                    color: '#000',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: adminOperationInProgress ? 'not-allowed' : 'pointer',
                    minWidth: '130px',
                    whiteSpace: 'nowrap'
                  }}
                >
                  {adminOperationInProgress ? 'Processing...' : 'Reprocess OCR'}
                </button>
                <button
                  onClick={handleResendToAzure}
                  disabled={adminOperationInProgress}
                  style={{
                    padding: '0.4rem 1.2rem',
                    fontSize: '0.8rem',
                    background: adminOperationInProgress ? '#ccc' : '#fd7e14',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: adminOperationInProgress ? 'not-allowed' : 'pointer',
                    minWidth: '150px',
                    whiteSpace: 'nowrap'
                  }}
                >
                  {adminOperationInProgress ? 'Processing...' : 'Resend to Azure'}
                </button>
                <button
                  onClick={handleRegenerateHighlights}
                  disabled={adminOperationInProgress}
                  style={{
                    padding: '0.4rem 1.2rem',
                    fontSize: '0.8rem',
                    background: adminOperationInProgress ? '#ccc' : '#6f42c1',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: adminOperationInProgress ? 'not-allowed' : 'pointer',
                    minWidth: '160px',
                    whiteSpace: 'nowrap'
                  }}
                  title="Regenerate PDF highlight annotations for non-stock items"
                >
                  {adminOperationInProgress ? 'Processing...' : 'Regen Highlights'}
                </button>
              </div>
            )}

            {/* Admin Operation Result Alert */}
            {adminOperationResult && (
              <div style={{
                marginTop: '10px',
                padding: '12px',
                borderRadius: '4px',
                background: adminOperationResult.type === 'success' ? '#d4edda' : '#f8d7da',
                color: adminOperationResult.type === 'success' ? '#155724' : '#721c24',
                border: `1px solid ${adminOperationResult.type === 'success' ? '#c3e6cb' : '#f5c6cb'}`,
                fontSize: '0.85rem',
                whiteSpace: 'pre-wrap',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'flex-start'
              }}>
                <span>{adminOperationResult.message}</span>
                <button
                  onClick={() => setAdminOperationResult(null)}
                  style={{
                    background: 'transparent',
                    border: 'none',
                    fontSize: '1.2rem',
                    cursor: 'pointer',
                    marginLeft: '10px',
                    color: 'inherit'
                  }}
                >
                  ×
                </button>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* LLM FEATURE — AI Assist suggestions panel — see LLM-MANIFEST.md for removal instructions */}
      {(aiAssistSuggestions || aiAssistError || aiReconciliationMatches.length > 0) && (
        <div style={{
          background: '#f8f5ff',
          border: '1px solid #d4c5f9',
          borderRadius: '8px',
          padding: '1rem',
          marginBottom: '1rem',
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
            <h3 style={{ margin: 0, color: '#5a32a3', fontSize: '1rem' }}>{'\u2728'} AI Analysis</h3>
            <button
              onClick={() => { setAiAssistSuggestions(null); setAiReconciliationMatches([]); setAiAssistError(null) }}
              style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.1rem', color: '#999' }}
            >{'\u2715'}</button>
          </div>

          {aiAssistError && (
            <div style={{ background: '#f8d7da', color: '#721c24', padding: '0.5rem 0.75rem', borderRadius: '4px', fontSize: '0.85rem', marginBottom: '0.5rem' }}>
              {aiAssistError}
            </div>
          )}

          {/* Supplier match */}
          {aiAssistSuggestions?.supplier_match && !invoice.supplier_id && (
            <div style={{ background: '#d1ecf1', padding: '0.5rem 0.75rem', borderRadius: '4px', marginBottom: '0.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontSize: '0.85rem', color: '#0c5460' }}>
                Supplier: <strong>{aiAssistSuggestions.supplier_match.name}</strong> ({(aiAssistSuggestions.supplier_match.confidence * 100).toFixed(0)}% confident) — {aiAssistSuggestions.supplier_match.reason}
              </span>
              <button
                onClick={() => handleApplyAiSupplier(aiAssistSuggestions.supplier_match!.id)}
                style={{ padding: '0.25rem 0.75rem', background: '#17a2b8', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8rem' }}
              >Accept</button>
            </div>
          )}

          {/* Total mismatch analysis */}
          {aiAssistSuggestions?.total_mismatch_analysis && (
            <div style={{ background: '#fff3cd', padding: '0.5rem 0.75rem', borderRadius: '4px', marginBottom: '0.5rem', fontSize: '0.85rem', color: '#856404' }}>
              <strong>Total mismatch:</strong> {aiAssistSuggestions.total_mismatch_analysis}
            </div>
          )}

          {/* VAT treatment */}
          {aiAssistSuggestions?.vat_treatment && (
            <div style={{ background: '#e2e3e5', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.5rem', fontSize: '0.85rem', color: '#383d41' }}>
              VAT treatment: <strong>{aiAssistSuggestions.vat_treatment}</strong>
            </div>
          )}

          {/* Line item corrections */}
          {aiAssistSuggestions?.line_item_corrections && aiAssistSuggestions.line_item_corrections.length > 0 && (
            <div style={{ marginBottom: '0.5rem' }}>
              <div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#5a32a3', marginBottom: '0.35rem' }}>Line Item Corrections</div>
              {aiAssistSuggestions.line_item_corrections
                .filter(c => !aiDismissedCorrections.has(`${c.idx}-${c.field}`))
                .map((c, i) => (
                <div key={i} style={{ background: '#ffe6e6', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85rem' }}>
                  <span style={{ color: '#721c24' }}>
                    Line {c.idx + 1}: <strong>{c.field}</strong> {c.current} {'\u2192'} {c.suggested} — {c.reason}
                  </span>
                  <div style={{ display: 'flex', gap: '0.35rem' }}>
                    <button
                      onClick={() => handleApplyAiCorrection(c.idx, c.field, c.suggested)}
                      style={{ padding: '0.2rem 0.5rem', background: '#28a745', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                    >Apply</button>
                    <button
                      onClick={() => setAiDismissedCorrections(prev => new Set([...prev, `${c.idx}-${c.field}`]))}
                      style={{ padding: '0.2rem 0.5rem', background: '#6c757d', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                    >Dismiss</button>
                  </div>
                </div>
              ))}
            </div>
          )}

          {/* Subtotal flags */}
          {aiAssistSuggestions?.subtotal_flags && aiAssistSuggestions.subtotal_flags.length > 0 && lineItems && (
            <div style={{ marginBottom: '0.5rem' }}>
              <div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#5a32a3', marginBottom: '0.35rem' }}>Subtotal / Fee Rows</div>
              {aiAssistSuggestions.subtotal_flags.map((idx) => {
                const item = lineItems[idx]
                if (!item) return null
                return (
                  <div key={idx} style={{ background: '#fff3cd', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85rem' }}>
                    <span style={{ color: '#856404' }}>
                      Line {idx + 1}: &quot;{item.description?.substring(0, 50)}&quot; — appears to be a subtotal or fee, not a product
                    </span>
                    {!item.is_non_stock && (
                      <button
                        onClick={() => updateLineItemMutation.mutate({ itemId: item.id, data: { is_non_stock: true } as Partial<LineItem> })}
                        style={{ padding: '0.2rem 0.5rem', background: '#f0ad4e', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                      >Mark Non-Stock</button>
                    )}
                  </div>
                )
              })}
            </div>
          )}

          {/* Description recommendations */}
          {aiAssistSuggestions?.description_recommendations && aiAssistSuggestions.description_recommendations.length > 0 && lineItems && (
            <div style={{ marginBottom: '0.5rem' }}>
              <div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#5a32a3', marginBottom: '0.35rem' }}>Description Recommendations</div>
              {aiAssistSuggestions.description_recommendations
                .filter(r => r.recommendation !== 'keep')
                .map((r, i) => {
                  const item = lineItems[r.idx]
                  if (!item || !item.description_alt) return null
                  return (
                    <div key={i} style={{ background: '#d1ecf1', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85rem' }}>
                      <span style={{ color: '#0c5460' }}>
                        Line {r.idx + 1}: Swap description recommended — {r.reason}
                      </span>
                      <button
                        onClick={() => updateLineItemMutation.mutate({
                          itemId: item.id,
                          data: { description: item.description_alt, description_alt: item.description } as Partial<LineItem>
                        })}
                        style={{ padding: '0.2rem 0.5rem', background: '#17a2b8', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                      >Swap</button>
                    </div>
                  )
                })}
            </div>
          )}

          {/* LLM FEATURE — Pack size suggestions */}
          {aiAssistSuggestions?.pack_size_suggestions && aiAssistSuggestions.pack_size_suggestions.length > 0 && lineItems && (
            <div style={{ marginBottom: '0.5rem' }}>
              <div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#5a32a3', marginBottom: '0.35rem' }}>Pack Size (from description)</div>
              {aiAssistSuggestions.pack_size_suggestions.map((p, i) => {
                const item = lineItems[p.idx]
                if (!item) return null
                const packLabel = p.pack_quantity > 1
                  ? `${p.pack_quantity} × ${p.unit_size}${p.unit_size_type}`
                  : `${p.unit_size}${p.unit_size_type}`
                const alreadySet = item.pack_quantity && item.unit_size
                return (
                  <div key={i} style={{ background: alreadySet ? '#e8e8e8' : '#e8daef', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85rem' }}>
                    <span style={{ color: alreadySet ? '#666' : '#4a235a' }}>
                      Line {p.idx + 1}: &quot;{item.description?.substring(0, 40)}&quot; → <strong>{packLabel}</strong>
                      {alreadySet && ' (already set)'}
                    </span>
                    {!alreadySet && (
                      <button
                        onClick={() => updateLineItemMutation.mutate({
                          itemId: item.id,
                          data: { pack_quantity: p.pack_quantity, unit_size: p.unit_size, unit_size_type: p.unit_size_type } as Partial<LineItem>
                        })}
                        style={{ padding: '0.2rem 0.5rem', background: '#7d3c98', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                      >Apply</button>
                    )}
                  </div>
                )
              })}
            </div>
          )}
          {/* END LLM FEATURE */}

          {/* Reconciliation matches */}
          {aiReconciliationMatches.length > 0 && lineItems && (
            <div>
              <div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#5a32a3', marginBottom: '0.35rem' }}>Ingredient Matches (from supplier history)</div>
              {aiReconciliationMatches.map((m, i) => {
                const item = lineItems[m.idx]
                if (!item) return null
                return (
                  <div key={i} style={{ background: '#d4edda', padding: '0.4rem 0.75rem', borderRadius: '4px', marginBottom: '0.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.85rem' }}>
                    <span style={{ color: '#155724' }}>
                      Line {m.idx + 1} &quot;{item.description?.substring(0, 40)}&quot; {'\u2192'} <strong>{m.ingredient_name}</strong> ({(m.confidence * 100).toFixed(0)}%) — {m.reason}
                    </span>
                    <button
                      onClick={() => updateLineItemMutation.mutate({ itemId: item.id, data: { ingredient_id: m.ingredient_id } as Partial<LineItem> })}
                      style={{ padding: '0.2rem 0.5rem', background: '#28a745', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.75rem' }}
                    >Map</button>
                  </div>
                )
              })}
            </div>
          )}
        </div>
      )}

      {/* Full-width Line Items Section */}
      <div style={styles.lineItemsSection}>
        <div style={{ marginBottom: '10px' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
            <h3 style={{ margin: '0' }}>Line Items</h3>
            <button
              onClick={() => {
                if (bulkEditMode) {
                  // Check if there are unsaved changes
                  if (editingLineItem !== null) {
                    if (!window.confirm('You have unsaved changes. Are you sure you want to exit edit mode?')) {
                      return
                    }
                  }
                  // Exiting bulk edit mode - cancel any unsaved edits
                  setEditingLineItem(null)
                  setLineItemEdits({})
                  setBulkEditMode(false)
                } else {
                  // Entering bulk edit mode
                  setBulkEditMode(true)
                  // If there are no line items, automatically add one
                  if (!lineItems || lineItems.length === 0) {
                    handleAddLineItem()
                  }
                }
              }}
              style={{
                ...styles.editBtn,
                background: bulkEditMode ? '#dc3545' : '#f0f0f0',
                color: bulkEditMode ? 'white' : '#333',
                border: bulkEditMode ? 'none' : '1px solid #ddd',
                padding: '0.5rem 1rem',
                fontSize: '0.85rem'
              }}
            >
              {bulkEditMode ? '✕ Exit Edit Mode' : '✏️ Edit Line Items'}
            </button>
          </div>
          {lineItems && lineItems.length > 0 && (
            <div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
              <input
                type="text"
                placeholder="Search by code or description..."
                value={lineItemSearchText}
                onChange={(e) => setLineItemSearchText(e.target.value)}
                style={{
                  flex: 1,
                  padding: '4px 8px',
                  borderRadius: '4px',
                  border: '1px solid #d1d5db',
                  fontSize: '0.9rem'
                }}
              />
              <label style={{ fontSize: '0.9rem', color: '#6b7280' }}>
                Price:
                <select
                  value={lineItemPriceFilter}
                  onChange={(e) => setLineItemPriceFilter(e.target.value)}
                  style={{ marginLeft: '5px', padding: '4px 8px', borderRadius: '4px', border: '1px solid #d1d5db' }}
                >
                  <option value="">All</option>
                  <option value="consistent" disabled={filterOptionCounts.consistent === 0}>✓ Consistent</option>
                  <option value="amber" disabled={filterOptionCounts.amber === 0}>? Changed</option>
                  <option value="red" disabled={filterOptionCounts.red === 0}>! Large change</option>
                  <option value="no_history" disabled={filterOptionCounts.no_history === 0}>No history</option>
                </select>
              </label>
              <label style={{ fontSize: '0.9rem', color: '#6b7280' }}>
                Portions:
                <select
                  value={lineItemPortionsFilter}
                  onChange={(e) => setLineItemPortionsFilter(e.target.value)}
                  style={{ marginLeft: '5px', padding: '4px 8px', borderRadius: '4px', border: '1px solid #d1d5db' }}
                >
                  <option value="">All</option>
                  <option value="yes" disabled={filterOptionCounts.withPortions === 0}>📦 Yes</option>
                  <option value="no" disabled={filterOptionCounts.withoutPortions === 0}>○ No</option>
                </select>
              </label>
              <label style={{ fontSize: '0.9rem', color: '#6b7280' }}>
                Data:
                <select
                  value={lineItemMissingDataFilter}
                  onChange={(e) => setLineItemMissingDataFilter(e.target.value)}
                  style={{ marginLeft: '5px', padding: '4px 8px', borderRadius: '4px', border: '1px solid #d1d5db' }}
                >
                  <option value="">All</option>
                  <option value="missing" disabled={filterOptionCounts.missingData === 0}>⚠ Missing key data</option>
                </select>
              </label>
              {(lineItemSortColumn || lineItemPriceFilter || lineItemSearchText || lineItemPortionsFilter || lineItemMissingDataFilter) && (
                <button
                  onClick={() => {
                    setLineItemSortColumn('')
                    setLineItemPriceFilter('')
                    setLineItemSearchText('')
                    setLineItemPortionsFilter('')
                    setLineItemMissingDataFilter('')
                  }}
                  style={{
                    padding: '4px 10px',
                    fontSize: '0.85rem',
                    color: '#6b7280',
                    background: '#f3f4f6',
                    border: '1px solid #d1d5db',
                    borderRadius: '4px',
                    cursor: 'pointer'
                  }}
                >
                  Clear
                </button>
              )}
            </div>
          )}
        </div>
        {(lineItems && lineItems.length > 0) || bulkEditMode ? (
          <div style={styles.lineItemsTableContainer} ref={lineItemsTableRef}>
          <table style={styles.lineItemsTable}>
            <thead style={styles.stickyThead}>
              <tr>
                <th style={{ ...styles.th, width: '30px' }}></th>
                <th
                  style={{ ...styles.th, width: '80px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('code')}
                >
                  Code {lineItemSortColumn === 'code' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th
                  style={{ ...styles.th, minWidth: '240px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('description')}
                >
                  Description {lineItemSortColumn === 'description' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th
                  style={{ ...styles.th, width: '50px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('unit')}
                >
                  Unit {lineItemSortColumn === 'unit' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th
                  style={{ ...styles.th, width: '50px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('quantity')}
                >
                  Qty {lineItemSortColumn === 'quantity' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th
                  style={{ ...styles.th, width: '110px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('unit_price')}
                >
                  Price {lineItemSortColumn === 'unit_price' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th style={{ ...styles.th, width: '50px' }}>Tax</th>
                <th
                  style={{ ...styles.th, width: '70px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => handleLineItemSort('amount')}
                >
                  Total {lineItemSortColumn === 'amount' && (lineItemSortDirection === 'asc' ? '↑' : '↓')}
                </th>
                <th
                  style={{ ...styles.th, textAlign: 'center', width: '60px', cursor: 'pointer', userSelect: 'none' }}
                  onClick={() => setShowBulkStockModal(true)}
                  title="Click to mark all items as stock or non-stock"
                >
                  Stock
                </th>
                <th style={{ ...styles.th, textAlign: 'center', width: '40px' }} title="Cost Breakdown"></th>
                <th style={{ ...styles.th, width: '70px' }}></th>
              </tr>
              {/* Sticky page indicator row for multi-page invoices */}
              {invoice && invoice.total_pages && invoice.total_pages > 1 && !lineItemSortColumn && !lineItemSearchText && !lineItemPriceFilter && !lineItemPortionsFilter && !lineItemMissingDataFilter && (
                <tr>
                  <th colSpan={11} style={styles.stickyPageIndicatorCell}>
                    Page {currentVisiblePage} of {invoice.total_pages}
                  </th>
                </tr>
              )}
            </thead>
            <tbody>
              {[...filteredAndSortedLineItems, ...(editingLineItem === -1 ? [{
                id: -1,
                product_code: lineItemEdits.product_code || '',
                description: lineItemEdits.description || '',
                description_alt: null,
                unit: lineItemEdits.unit || '',
                quantity: lineItemEdits.quantity ?? null,
                order_quantity: null,
                unit_price: lineItemEdits.unit_price ?? null,
                tax_rate: lineItemEdits.tax_rate || '',
                tax_amount: null,
                amount: lineItemEdits.amount ?? null,
                line_number: lineItemEdits.line_number || 0,
                is_non_stock: lineItemEdits.is_non_stock || false,
                raw_content: null,
                pack_quantity: null,
                unit_size: null,
                unit_size_type: null,
                portions_per_unit: null,
                cost_per_item: null,
                cost_per_portion: null,
                ocr_warnings: null,
                price_change_status: null,
                price_change_percent: null,
                previous_price: null,
                future_price: null,
                future_change_percent: null,
                page_number: null,
                _originalIndex: -1
              }] : [])].map((item, index, allItems) => {
                const bbox = getLineItemBoundingBox(item._originalIndex)
                const highQtyThreshold = settings?.high_quantity_threshold ?? 100
                const isHighQuantity = item.quantity !== null && item.quantity > highQtyThreshold

                // Check for stock status conflict
                const itemStockHistory = stockHistory?.[item.id.toString()]
                const hasStockConflict = itemStockHistory?.has_history &&
                  itemStockHistory.previously_non_stock &&
                  !item.is_non_stock // Current item is marked as stock, but was previously non-stock

                // Check for price calculation errors
                // Valid cases: qty only (free item), or all three values with correct calculation
                // For credit notes, totals are negative but qty/price are positive
                const hasQty = item.quantity != null && item.quantity !== 0
                const hasPrice = item.unit_price != null && item.unit_price !== 0
                const hasTotal = item.amount != null && item.amount !== 0

                // qty * price should equal amount (works for both positive and negative/credit lines)
                const expectedTotal = (item.quantity || 0) * (item.unit_price || 0)
                const actualTotal = item.amount || 0
                const calculationMismatch = Math.abs(expectedTotal - actualTotal) > 0.02

                const hasPriceCalcError =
                  (hasPrice && (!hasQty || !hasTotal)) ||  // price requires both qty and total
                  (hasTotal && (!hasQty || !hasPrice)) ||  // total requires both qty and price
                  (hasQty && hasPrice && hasTotal && calculationMismatch) // calculation mismatch (2p tolerance)

                // Check if this line item is disputed
                const isDisputed = invoice.disputed_line_item_ids?.includes(item.id)

                // Check if this is the last item on its page (for page break indicator)
                const isLastOnPage = invoice && invoice.total_pages && invoice.total_pages > 1 &&
                  !lineItemSortColumn && !lineItemSearchText && !lineItemPriceFilter && !lineItemPortionsFilter && !lineItemMissingDataFilter &&
                  item.page_number && index < allItems.length - 1 &&
                  allItems[index + 1]?.page_number !== item.page_number

                // High quantity is lighter amber, disputed is pale pink (OCR warning now shown on product code cell only)
                const rowBackground = isHighQuantity ? '#fff8e1' : isDisputed ? '#fff0f3' : undefined
                const rowStyle: React.CSSProperties | undefined = rowBackground ? { backgroundColor: rowBackground } : undefined
                // Add amber border for stock conflicts, or dashed border for page breaks
                let rowStyleWithBorder: React.CSSProperties | undefined = hasStockConflict
                  ? { ...rowStyle, border: '2px solid #ffc107' }
                  : rowStyle
                if (isLastOnPage && !hasStockConflict) {
                  rowStyleWithBorder = { ...rowStyleWithBorder, borderBottom: '2px dashed #adb5bd' }
                }

                // Style for cells with price calculation errors - create a single box spanning qty to total
                const errorCellStyleLeft = hasPriceCalcError
                  ? { borderLeft: '2px solid #dc3545', borderTop: '2px solid #dc3545', borderBottom: '2px solid #dc3545', backgroundColor: '#ffe6e6' }
                  : {}
                const errorCellStyleMiddle = hasPriceCalcError
                  ? { borderTop: '2px solid #dc3545', borderBottom: '2px solid #dc3545', backgroundColor: '#ffe6e6' }
                  : {}
                const errorCellStyleRight = hasPriceCalcError
                  ? { borderRight: '2px solid #dc3545', borderTop: '2px solid #dc3545', borderBottom: '2px solid #dc3545', backgroundColor: '#ffe6e6' }
                  : {}

                return (
                  <React.Fragment key={item.id}>
                    <tr style={rowStyleWithBorder} data-page={item.page_number || 1}>
                      <td style={styles.td}>
                        {bbox && (
                          <button
                            type="button"
                            onClick={() => handleHighlightLineItem(item.id, item._originalIndex)}
                            style={{
                              ...styles.eyeBtn,
                              padding: '0.25rem 0.4rem',
                              fontSize: '0.8rem',
                              ...(expandedLineItem === item.id ? styles.eyeBtnActive : {})
                            }}
                            title="Show on document"
                          >
                            👁
                          </button>
                        )}
                      </td>
                      {editingLineItem === item.id ? (
                        <>
                          <td style={styles.td}>
                            <input
                              type="text"
                              value={editingLineItem === item.id ? (lineItemEdits.product_code || '') : (item.product_code || '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, product_code: e.target.value }); }}
                              style={{ ...styles.tableInput, width: '70px' }}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="text"
                              value={editingLineItem === item.id ? (lineItemEdits.description || '') : (item.description || '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, description: e.target.value }); }}
                              style={styles.tableInput}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="text"
                              value={editingLineItem === item.id ? (lineItemEdits.unit || '') : (item.unit || '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, unit: e.target.value }); }}
                              style={{ ...styles.tableInput, width: '50px' }}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="number"
                              step="0.01"
                              value={editingLineItem === item.id ? (lineItemEdits.quantity ?? '') : (item.quantity ?? '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, quantity: parseFloat(e.target.value) }); }}
                              style={{ ...styles.tableInput, width: '50px' }}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="number"
                              step="0.01"
                              value={editingLineItem === item.id ? (lineItemEdits.unit_price ?? '') : (item.unit_price ?? '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, unit_price: parseFloat(e.target.value) }); }}
                              style={{ ...styles.tableInput, width: '70px' }}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="text"
                              value={editingLineItem === item.id ? (lineItemEdits.tax_rate || '') : (item.tax_rate || '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, tax_rate: e.target.value }); }}
                              style={{ ...styles.tableInput, width: '50px' }}
                            />
                          </td>
                          <td style={styles.td}>
                            <input
                              type="number"
                              step="0.01"
                              value={editingLineItem === item.id ? (lineItemEdits.amount ?? '') : (item.amount ?? '')}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, amount: parseFloat(e.target.value) }); }}
                              style={{ ...styles.tableInput, width: '60px' }}
                            />
                          </td>
                          <td style={{ ...styles.td, textAlign: 'center' }}>
                            <input
                              type="checkbox"
                              checked={editingLineItem === item.id ? !lineItemEdits.is_non_stock : !item.is_non_stock}
                              onChange={(e) => { ensureEditing(item); setLineItemEdits({ ...lineItemEdits, is_non_stock: !e.target.checked }); }}
                              style={{ width: '18px', height: '18px', cursor: 'pointer' }}
                            />
                          </td>
                          <td style={{ ...styles.td, textAlign: 'center' }}>
                            {/* Scale icon disabled during edit */}
                          </td>
                          <td style={styles.td}>
                            <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
                              <button
                                onClick={() => saveLineItemEdit(item.id)}
                                style={{
                                  padding: '0.35rem 0.5rem',
                                  background: '#f0f0f0',
                                  border: '1px solid #ddd',
                                  borderRadius: '4px',
                                  cursor: 'pointer',
                                  fontSize: '1rem',
                                  display: 'flex',
                                  alignItems: 'center',
                                  justifyContent: 'center',
                                  minWidth: '32px',
                                  height: '32px'
                                }}
                                title="Save changes"
                              >
                                ✅
                              </button>
                              <button
                                onClick={cancelEditLineItem}
                                style={{
                                  padding: '0.35rem 0.5rem',
                                  background: '#f0f0f0',
                                  border: '1px solid #ddd',
                                  borderRadius: '4px',
                                  cursor: 'pointer',
                                  fontSize: '1rem',
                                  display: 'flex',
                                  alignItems: 'center',
                                  justifyContent: 'center',
                                  minWidth: '32px',
                                  height: '32px'
                                }}
                                title="Cancel editing"
                              >
                                ❌
                              </button>
                            </div>
                          </td>
                        </>
                      ) : (
                        <>
                          <td style={{ ...styles.td, fontSize: '0.85rem', color: '#666' }}>
                            <span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
                              <span
                                style={item.ocr_warnings ? {
                                  background: '#fff3cd',
                                  padding: '2px 6px',
                                  borderRadius: '3px',
                                  border: '1px solid #ffc107'
                                } : undefined}
                              >
                                {item.product_code || '—'}
                              </span>
                              {item.ocr_warnings && (
                                <span
                                  title={item.ocr_warnings}
                                  style={{
                                    cursor: 'help',
                                    fontSize: '1rem'
                                  }}
                                >
                                  ⚠️
                                </span>
                              )}
                            </span>
                          </td>
                          <td style={{ ...styles.td, ...(item.is_non_stock ? { color: '#856404', fontStyle: 'italic' } : {}) }}>
                            <div>
                              {getFirstLineOfDescription(item.description) || '—'}
                              {item.description_alt && item.description !== item.description_alt && (
                                <button
                                  onClick={(e) => { e.stopPropagation(); setDescSwapItem(item); }}
                                  style={{
                                    background: 'none',
                                    border: 'none',
                                    cursor: 'pointer',
                                    padding: '2px 6px',
                                    fontSize: '0.9rem',
                                    color: '#e94560',
                                    marginLeft: '6px'
                                  }}
                                  title="OCR detected alternative description - click to compare"
                                >
                                  🔄
                                </button>
                              )}
                              {invoice.disputed_line_item_ids?.includes(item.id) && (
                                <span style={{
                                  marginLeft: '8px',
                                  color: '#e94560',
                                  fontSize: '0.7rem',
                                  fontWeight: 'bold',
                                  padding: '2px 6px',
                                  background: '#ffe0e6',
                                  borderRadius: '3px'
                                }}>
                                  [DISPUTED]
                                </span>
                              )}
                            </div>
                            {hasStockConflict && (
                              <div style={{
                                fontSize: '0.75rem',
                                color: '#856404',
                                fontStyle: 'italic',
                                marginTop: '4px',
                                lineHeight: '1.2'
                              }}>
                                ⚠️ This item has previously been marked as not a stock item
                              </div>
                            )}
                            {hasPriceCalcError && (
                              <div style={{
                                fontSize: '0.75rem',
                                color: '#dc3545',
                                fontWeight: 'bold',
                                marginTop: '4px',
                                lineHeight: '1.3'
                              }}>
                                ! QTY x Price = Total Check Failed<br />
                                {item.quantity?.toFixed(2)} × £{item.unit_price?.toFixed(2)} ≠ £{item.amount?.toFixed(2)}
                              </div>
                            )}
                            {/* Description alias suggestion banner */}
                            {!item.ingredient_id && !item.product_code && item.description && (() => {
                              const firstLine = item.description.split('\n')[0].trim().toLowerCase()
                              const suggestion = aliasSuggestions[firstLine]
                              if (!suggestion) return null
                              const isAdding = addingAliasFor === firstLine
                              return (
                                <div style={{
                                  fontSize: '0.75rem',
                                  color: '#0c5460',
                                  background: '#d1ecf1',
                                  padding: '0.25rem 0.5rem',
                                  borderRadius: '4px',
                                  marginTop: '4px',
                                  border: '1px solid #bee5eb',
                                  display: 'flex',
                                  alignItems: 'center',
                                  justifyContent: 'space-between',
                                  gap: '0.5rem',
                                  flexWrap: 'wrap',
                                }}>
                                  <span>
                                    Similar to "{suggestion.canonical_description}"
                                    {suggestion.ingredient_name && <> → {suggestion.ingredient_name}</>}
                                    {' '}({Math.round(suggestion.similarity * 100)}% match)
                                    {suggestion.price_difference != null && suggestion.price_difference > 5 && (
                                      <span style={{ color: '#856404', marginLeft: '6px' }}>
                                        Price differs {suggestion.price_difference.toFixed(0)}%
                                      </span>
                                    )}
                                  </span>
                                  <button
                                    type="button"
                                    onClick={(e) => {
                                      e.stopPropagation()
                                      setAddingAliasFor(firstLine)
                                      addDescriptionAliasMutation.mutate({
                                        sourceId: suggestion.source_id,
                                        alias: item.description!.split('\n')[0].trim(),
                                      })
                                    }}
                                    disabled={isAdding}
                                    style={{
                                      padding: '0.2rem 0.5rem',
                                      background: '#17a2b8',
                                      color: 'white',
                                      border: 'none',
                                      borderRadius: '4px',
                                      cursor: isAdding ? 'default' : 'pointer',
                                      fontSize: '0.7rem',
                                      fontWeight: '500',
                                      whiteSpace: 'nowrap' as const,
                                      opacity: isAdding ? 0.6 : 1,
                                    }}
                                  >
                                    {isAdding ? 'Adding...' : '+ Add as alias'}
                                  </button>
                                </div>
                              )
                            })()}
                          </td>
                          <td style={{ ...styles.td, fontSize: '0.85rem' }}>{item.unit || '—'}</td>
                          <td style={{ ...styles.td, ...errorCellStyleLeft }}>{item.quantity?.toFixed(2) || '—'}</td>
                          <td style={{ ...styles.td, ...errorCellStyleMiddle, position: 'relative' }}>
                            {/* Price value - inline-block to align with other cells */}
                            <div style={{ display: 'inline-block', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>
                              {item.unit_price != null ? `£${item.unit_price.toFixed(2)}` : '—'}
                              {/* Show green tick inline for consistent prices */}
                              {item.price_change_status === 'consistent' && (
                                <span style={{ marginLeft: '6px', color: '#22c55e', fontWeight: 'bold' }}>
                                  ✓
                                </span>
                              )}
                              {/* Show green document icon inline for new items (no history) */}
                              {item.price_change_status === 'no_history' && (
                                <span style={{ marginLeft: '6px', color: '#22c55e' }}>
                                  📄⁺
                                </span>
                              )}
                              {/* Show amber coin icon inline for items with price changes */}
                              {item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_status !== 'consistent' && (
                                <span style={{ marginLeft: '6px', color: '#f59e0b' }}>
                                  🪙
                                </span>
                              )}
                            </div>
                            {/* Show arrow and percentage below for price changes - absolutely positioned */}
                            {item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_status !== 'consistent' && item.price_change_percent !== null && item.price_change_percent !== 0 && (() => {
                              const isIncrease = item.price_change_percent > 0
                              const arrow = isIncrease ? '▲' : '▼'
                              const color = isIncrease ? '#ef4444' : '#22c55e'
                              return (
                                <div
                                  onClick={() => openPriceHistoryModal(item)}
                                  style={{
                                    position: 'absolute',
                                    bottom: '0.2rem',
                                    left: '0.5rem',
                                    fontSize: '0.65rem',
                                    lineHeight: '1',
                                    color,
                                    cursor: 'pointer'
                                  }}
                                  title="View price history"
                                >
                                  <span style={{ fontWeight: 'bold' }}>{arrow}</span>{' '}
                                  {Math.abs(item.price_change_percent).toFixed(1)}%
                                </div>
                              )
                            })()}
                            {/* Show future price change in brackets and grey for old invoices */}
                            {item.future_change_percent !== null && item.future_change_percent !== 0 && (() => {
                              const isIncrease = item.future_change_percent > 0
                              const arrow = isIncrease ? '▲' : '▼'
                              return (
                                <div
                                  onClick={() => openPriceHistoryModal(item)}
                                  style={{
                                    position: 'absolute',
                                    bottom: '0.2rem',
                                    left: '0.5rem',
                                    fontSize: '0.75rem',
                                    color: '#9ca3af',
                                    cursor: 'pointer'
                                  }}
                                  title="Price changed after this invoice"
                                >
                                  (<span style={{ fontWeight: 'bold' }}>{arrow}</span>{' '}
                                  {Math.abs(item.future_change_percent).toFixed(1)}%)
                                </div>
                              )
                            })()}
                          </td>
                          <td style={{ ...styles.td, fontSize: '0.85rem', ...errorCellStyleMiddle }}>{item.tax_rate || '—'}</td>
                          <td style={{ ...styles.td, ...errorCellStyleRight }}>{item.amount ? `£${item.amount.toFixed(2)}` : '—'}</td>
                          <td style={{ ...styles.td, textAlign: 'center' }}>
                            <input
                              type="checkbox"
                              checked={!item.is_non_stock}
                              onChange={(e) => {
                                updateLineItemMutation.mutate({
                                  itemId: item.id,
                                  data: { is_non_stock: !e.target.checked }
                                })
                              }}
                              style={{ width: '18px', height: '18px', cursor: 'pointer' }}
                              title={!item.is_non_stock ? 'Mark as non-stock item' : 'Mark as stock item'}
                            />
                          </td>
                          <td style={{ ...styles.td, textAlign: 'center' }}>
                            <button
                              type="button"
                              onClick={() => toggleCostBreakdown(item)}
                              style={{
                                ...styles.scaleBtn,
                                ...(ingredientModalItem?.id === item.id ? styles.scaleBtnActive : {}),
                                color: getScaleIconColor(item)
                              }}
                              title={
                                item.ingredient_id
                                  ? `Mapped → ${item.ingredient_name || 'Ingredient'}`
                                  : !item.pack_quantity
                                    ? 'No pack data - click to map ingredient'
                                    : item.portions_per_unit === null
                                      ? 'Pack data extracted - click to map ingredient'
                                      : 'Pack data complete - click to map ingredient'
                              }
                            >
                              <ScaleIcon />
                            </button>
                          </td>
                          <td style={{ ...styles.td, minWidth: '135px', whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
                            <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
                              {bulkEditMode ? (
                                <>
                                  <button onClick={() => startEditLineItem(item)} style={styles.iconBtn} title="Edit line item">
                                    ✏️
                                  </button>
                                  <button
                                    onClick={() => handleDeleteLineItem(item.id)}
                                    style={{
                                      ...styles.iconBtn,
                                      color: '#dc3545'
                                    }}
                                    title="Delete line item"
                                  >
                                    🗑️
                                  </button>
                                </>
                              ) : (
                                <>
                                  <button onClick={() => openSearchModal(item)} style={styles.iconBtn} title="Search similar items">
                                    🔍
                                  </button>
                                  <button
                                    onClick={() => item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_percent !== null && openPriceHistoryModal(item)}
                                    style={{
                                      ...styles.iconBtn,
                                      opacity: (item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_percent !== null) ? 1 : 0.5,
                                      cursor: (item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_percent !== null) ? 'pointer' : 'not-allowed'
                                    }}
                                    title={
                                      (item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_percent !== null)
                                        ? "View price history"
                                        : "No price history available"
                                    }
                                    disabled={!(item.price_change_status && item.price_change_status !== 'no_history' && item.price_change_percent !== null)}
                                  >
                                    📊
                                  </button>
                                </>
                              )}
                            </div>
                          </td>
                        </>
                      )}
                    </tr>
                    {expandedLineItem === item.id && bbox && (
                      <tr>
                        <td colSpan={11} style={styles.lineItemPreviewCell}>
                          {isPDF && pdfPages.length > 0 ? (
                            (() => {
                              // Get the correct page canvas (high-res)
                              const pageIndex = bbox.pageNumber - 1
                              const pageData = pdfPages[pageIndex]
                              if (!pageData) return null

                              // Calculate bounding box coordinates in high-res canvas pixels
                              const bboxX = (bbox.x / 100) * pageData.width
                              const bboxY = (bbox.y / 100) * pageData.height
                              const bboxW = (bbox.width / 100) * pageData.width
                              const bboxH = (bbox.height / 100) * pageData.height

                              // Small padding around OCR text so box doesn't cover the text
                              const boxPadding = 15 // 15px breathing room around detected text
                              const expandedBboxX = Math.max(0, bboxX - boxPadding)
                              const expandedBboxY = Math.max(0, bboxY - boxPadding)
                              const expandedBboxW = bboxW + (boxPadding * 2)
                              const expandedBboxH = bboxH + (boxPadding * 2)

                              // Crop tightly to the expanded box - minimal extra space
                              const cropPadding = 5 // Very tight crop so box fills container
                              const startX = Math.max(0, expandedBboxX - cropPadding)
                              const startY = Math.max(0, expandedBboxY - cropPadding)
                              const endX = Math.min(pageData.width, expandedBboxX + expandedBboxW + cropPadding)
                              const endY = Math.min(pageData.height, expandedBboxY + expandedBboxH + cropPadding)
                              const cropW = endX - startX
                              const cropH = endY - startY

                              // Calculate expanded bounding box position relative to cropped area
                              const relBboxX = expandedBboxX - startX
                              const relBboxY = expandedBboxY - startY

                              // Create a cropped canvas
                              const croppedCanvas = document.createElement('canvas')
                              croppedCanvas.width = cropW
                              croppedCanvas.height = cropH
                              const ctx = croppedCanvas.getContext('2d')
                              if (ctx) {
                                ctx.drawImage(
                                  pageData.canvas,
                                  startX, startY, cropW, cropH,
                                  0, 0, cropW, cropH
                                )
                              }

                              return (
                                <div style={styles.lineItemPreviewContainer}>
                                  <div style={{
                                    position: 'relative',
                                    display: 'block',
                                    width: '100%',
                                  }}>
                                    <img
                                      src={croppedCanvas.toDataURL()}
                                      alt="Line item from invoice"
                                      style={{
                                        width: '100%',
                                        height: 'auto',
                                        display: 'block',
                                      }}
                                    />
                                    {/* Bounding box overlay */}
                                    <div
                                      style={{
                                        position: 'absolute',
                                        left: `${(relBboxX / cropW) * 100}%`,
                                        top: `${(relBboxY / cropH) * 100}%`,
                                        width: `${(expandedBboxW / cropW) * 100}%`,
                                        height: `${(expandedBboxH / cropH) * 100}%`,
                                        border: '2px solid #ffc107',
                                        borderRadius: '2px',
                                        pointerEvents: 'none',
                                        boxShadow: '0 0 8px rgba(255, 193, 7, 0.5)',
                                      }}
                                    />
                                  </div>
                                </div>
                              )
                            })()
                          ) : imageUrl ? (
                            (() => {
                              // Expand bounding box for better visibility (percentage-based)
                              const expansionPercent = 2 // Expand by 2% in each direction
                              const expandedX = Math.max(0, bbox.x - expansionPercent)
                              const expandedY = Math.max(0, bbox.y - expansionPercent)
                              const expandedWidth = Math.min(100 - expandedX, bbox.width + (expansionPercent * 2))
                              const expandedHeight = Math.min(100 - expandedY, bbox.height + (expansionPercent * 2))

                              return (
                                <div style={styles.lineItemPreviewContainer}>
                                  <div style={{
                                    position: 'relative',
                                    display: 'block',
                                    width: '100%',
                                  }}>
                                    <img
                                      src={`${imageUrl.split('?')[0]}`}
                                      alt="Line item location"
                                      style={{
                                        width: '100%',
                                        height: 'auto',
                                        display: 'block',
                                      }}
                                    />
                                    {/* Bounding box overlay */}
                                    <div
                                      style={{
                                        position: 'absolute',
                                        left: `${expandedX}%`,
                                        top: `${expandedY}%`,
                                        width: `${expandedWidth}%`,
                                        height: `${expandedHeight}%`,
                                        border: '2px solid #ffc107',
                                        borderRadius: '2px',
                                        pointerEvents: 'none',
                                        boxShadow: '0 0 8px rgba(255, 193, 7, 0.5)',
                                      }}
                                    />
                                  </div>
                                </div>
                              )
                            })()
                          ) : null}
                        </td>
                      </tr>
                    )}
                  </React.Fragment>
                )
              })}
              {bulkEditMode && (
                <tr>
                  <td colSpan={11} style={{ textAlign: 'center', padding: '1.5rem', background: '#f9fafb' }}>
                    <button
                      onClick={handleAddLineItem}
                      style={{
                        ...styles.confirmBtn,
                        padding: '0.75rem 2rem',
                        fontSize: '0.95rem'
                      }}
                    >
                      + Add Line Item
                    </button>
                  </td>
                </tr>
              )}
            </tbody>
          </table>
          </div>
        ) : (
          <p style={styles.noItems}>No line items extracted</p>
        )}

        {/* Line Items Total Validation */}
        <LineItemsValidation
          lineItems={lineItems || []}
          invoiceTotal={parseFloat(total) || 0}
          netTotal={netTotal ? parseFloat(netTotal) : null}
        />
      </div>

      {/* Delete Confirmation Modal */}
      {showDeleteModal && (
        <div style={styles.modalOverlay} onClick={() => setShowDeleteModal(false)}>
          <div style={styles.modal} onClick={(e) => e.stopPropagation()}>
            <h3>Delete Invoice?</h3>
            <div style={styles.modalInfo}>
              <p><strong>Invoice #:</strong> {invoice.invoice_number || 'N/A'}</p>
              <p><strong>Date:</strong> {invoice.invoice_date || 'N/A'}</p>
              <p><strong>Total:</strong> {invoice.total ? `£${Number(invoice.total).toFixed(2)}` : 'N/A'}</p>
            </div>
            {invoice.duplicate_status && (
              <div style={styles.modalDuplicateInfo}>
                This invoice is marked as a {invoice.duplicate_status === 'firm_duplicate' ? 'duplicate' : 'possible duplicate'}.
                Deleting it will resolve the duplicate warning.
              </div>
            )}
            <div style={styles.modalActions}>
              <button onClick={() => setShowDeleteModal(false)} style={styles.cancelBtn}>Cancel</button>
              <button onClick={handleDelete} style={styles.confirmDeleteBtn}>Delete</button>
            </div>
          </div>
        </div>
      )}

      {/* Dext Send Confirmation Modal */}
      {showDextSendConfirm && (
        <div style={styles.modalOverlay} onClick={() => setShowDextSendConfirm(false)}>
          <div style={styles.modal} onClick={(e) => e.stopPropagation()}>
            <h3>Send Invoice to Dext?</h3>
            <p style={{ marginBottom: '1rem' }}>
              This will email the invoice PDF to your configured Dext address.
            </p>
            {invoiceNotes && (
              <p style={{ fontSize: '0.9rem', color: '#666' }}>
                <strong>Note:</strong> Invoice notes will be included in the email.
              </p>
            )}
            {lineItems && lineItems.some(item => item.is_non_stock) && (
              <p style={{ fontSize: '0.9rem', color: '#666' }}>
                <strong>Note:</strong> Non-stock items table will be included in the email.
              </p>
            )}
            <div style={styles.modalActions}>
              <button onClick={() => setShowDextSendConfirm(false)} style={styles.cancelBtn}>
                Cancel
              </button>
              <button
                onClick={async () => {
                  try {
                    const res = await fetch(`/kitchen/api/invoices/${id}/send-to-dext`, {
                      method: 'POST',
                      credentials: 'include'
                    })
                    if (res.ok) {
                      const result = await res.json()
                      alert(`Invoice sent to Dext successfully at ${new Date(result.sent_at).toLocaleString()}`)
                      setShowDextSendConfirm(false)
                      queryClient.invalidateQueries({ queryKey: ['invoice', id] })
                    } else {
                      const error = await res.json()
                      alert(`Failed to send: ${error.detail}`)
                    }
                  } catch (error) {
                    console.error('Failed to send to Dext:', error)
                    alert('Failed to send invoice to Dext')
                  }
                }}
                style={styles.confirmBtn}
              >
                Send to Dext
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Duplicate Comparison Modal */}
      {showDuplicateModal && duplicateInfo && (
        <div style={styles.modalOverlay} onClick={() => setShowDuplicateModal(false)}>
          <div style={styles.wideModal} onClick={(e) => e.stopPropagation()}>
            <h3>Duplicate Comparison</h3>
            <div style={styles.compareGrid}>
              <div style={styles.compareCard}>
                <h4>Current Invoice</h4>
                <p><strong>Invoice #:</strong> {duplicateInfo.current_invoice.invoice_number || '—'}</p>
                <p><strong>Date:</strong> {duplicateInfo.current_invoice.invoice_date || '—'}</p>
                <p><strong>Total:</strong> {duplicateInfo.current_invoice.total ? `£${Number(duplicateInfo.current_invoice.total).toFixed(2)}` : '—'}</p>
                <p><strong>Order #:</strong> {duplicateInfo.current_invoice.order_number || '—'}</p>
                <p><strong>Type:</strong> {duplicateInfo.current_invoice.document_type || 'invoice'}</p>
              </div>

              {duplicateInfo.firm_duplicate && (
                <div style={{ ...styles.compareCard, borderColor: '#dc3545' }}>
                  <h4 style={{ color: '#dc3545' }}>Exact Duplicate</h4>
                  <p><strong>Invoice #:</strong> {duplicateInfo.firm_duplicate.invoice_number || '—'}</p>
                  <p><strong>Date:</strong> {duplicateInfo.firm_duplicate.invoice_date || '—'}</p>
                  <p><strong>Total:</strong> {duplicateInfo.firm_duplicate.total ? `£${Number(duplicateInfo.firm_duplicate.total).toFixed(2)}` : '—'}</p>
                  <p><strong>Order #:</strong> {duplicateInfo.firm_duplicate.order_number || '—'}</p>
                  <p><strong>Type:</strong> {duplicateInfo.firm_duplicate.document_type || 'invoice'}</p>
                  <button
                    onClick={() => navigate(`/invoice/${duplicateInfo.firm_duplicate!.id}`)}
                    style={styles.viewBtn}
                  >
                    View This Invoice
                  </button>
                </div>
              )}

              {duplicateInfo.possible_duplicates.map((dup) => (
                <div key={dup.id} style={{ ...styles.compareCard, borderColor: '#ffc107' }}>
                  <h4 style={{ color: '#856404' }}>Possible Duplicate</h4>
                  <p><strong>Invoice #:</strong> {dup.invoice_number || '—'}</p>
                  <p><strong>Date:</strong> {dup.invoice_date || '—'}</p>
                  <p><strong>Total:</strong> {dup.total ? `£${Number(dup.total).toFixed(2)}` : '—'}</p>
                  <p><strong>Order #:</strong> {dup.order_number || '—'}</p>
                  <button
                    onClick={() => navigate(`/invoice/${dup.id}`)}
                    style={styles.viewBtn}
                  >
                    View This Invoice
                  </button>
                </div>
              ))}

              {duplicateInfo.related_documents.map((doc) => (
                <div key={doc.id} style={{ ...styles.compareCard, borderColor: '#17a2b8' }}>
                  <h4 style={{ color: '#17a2b8' }}>Related {doc.document_type === 'delivery_note' ? 'Delivery Note' : 'Invoice'}</h4>
                  <p><strong>Invoice #:</strong> {doc.invoice_number || '—'}</p>
                  <p><strong>Date:</strong> {doc.invoice_date || '—'}</p>
                  <p><strong>Total:</strong> {doc.total ? `£${Number(doc.total).toFixed(2)}` : '—'}</p>
                  <p><strong>Order #:</strong> {doc.order_number || '—'}</p>
                  <button
                    onClick={() => navigate(`/invoice/${doc.id}`)}
                    style={styles.viewBtn}
                  >
                    View This Invoice
                  </button>
                </div>
              ))}
            </div>
            <button onClick={() => setShowDuplicateModal(false)} style={styles.closeBtn}>Close</button>
          </div>
        </div>
      )}

      {/* Create Supplier Modal */}
      {showCreateSupplierModal && (
        <div style={styles.modalOverlay} onClick={() => setShowCreateSupplierModal(false)}>
          <div style={styles.modal} onClick={(e) => e.stopPropagation()}>
            <h3>Create New Supplier</h3>
            <label style={styles.modalLabel}>
              Supplier Name
              <input
                type="text"
                value={newSupplierName}
                onChange={(e) => setNewSupplierName(e.target.value)}
                style={styles.input}
                placeholder="e.g., Sysco, US Foods"
                autoFocus
              />
            </label>
            <div style={styles.modalActions}>
              <button onClick={() => setShowCreateSupplierModal(false)} style={styles.cancelBtn}>
                Cancel
              </button>
              <button
                onClick={handleCreateSupplier}
                style={styles.saveBtn}
                disabled={!newSupplierName.trim() || createSupplierMutation.isPending}
              >
                {createSupplierMutation.isPending ? 'Creating...' : 'Create & Select'}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Raw OCR Data Modal */}
      {showRawOcrModal && (
        <div style={styles.modalOverlay} onClick={() => setShowRawOcrModal(false)}>
          <div style={styles.rawOcrModal} onClick={(e) => e.stopPropagation()}>
            <h3>Raw Azure OCR Data</h3>
            <p style={styles.rawOcrHint}>
              This shows the raw data extracted by Azure. Use this to debug field extraction issues
              or identify custom field names that need mapping.
            </p>
            {rawOcrData?.raw_json ? (
              <div style={styles.rawOcrContent}>
                <h4>Extracted Fields</h4>
                {rawOcrData.raw_json.documents?.map((doc: any, docIdx: number) => (
                  <div key={docIdx} style={styles.rawOcrDoc}>
                    <h5>Document {docIdx + 1} (Confidence: {(doc.confidence * 100).toFixed(1)}%)</h5>
                    <table style={styles.rawOcrTable}>
                      <thead>
                        <tr>
                          <th style={styles.rawOcrTh}>Field</th>
                          <th style={styles.rawOcrTh}>Value</th>
                          <th style={styles.rawOcrTh}>Content</th>
                        </tr>
                      </thead>
                      <tbody>
                        {Object.entries(doc.fields || {}).map(([fieldName, field]: [string, any]) => (
                          <tr key={fieldName}>
                            <td style={styles.rawOcrTd}><strong>{fieldName}</strong></td>
                            <td style={styles.rawOcrTd}>
                              {typeof field?.value === 'object'
                                ? JSON.stringify(field?.value, null, 2)
                                : String(field?.value ?? '—')}
                            </td>
                            <td style={styles.rawOcrTd}>{field?.content || '—'}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                ))}
              </div>
            ) : (
              <p style={{ color: '#999' }}>No raw OCR data available for this invoice.</p>
            )}
            <button onClick={() => setShowRawOcrModal(false)} style={styles.closeBtn}>Close</button>
          </div>
        </div>
      )}

      {/* Non-stock confirmation modal */}
      {showNonStockConfirmModal && (
        <div style={styles.modalOverlay} onClick={() => setShowNonStockConfirmModal(false)}>
          <div style={{ ...styles.rawOcrModal, maxWidth: '480px' }} onClick={(e) => e.stopPropagation()}>
            <h3 style={{ marginTop: 0, marginBottom: '0.75rem' }}>Previously Non-Stock Items</h3>
            <p style={{ fontSize: '0.9rem', color: '#555', marginBottom: '1rem', lineHeight: 1.5 }}>
              The following item{nonStockConflictItems.length !== 1 ? 's have' : ' has'} been marked as <strong>non-stock</strong> on previous invoices from this supplier but {nonStockConflictItems.length !== 1 ? 'are' : 'is'} currently included in the stock cost for this invoice. Please confirm they have been checked.
            </p>
            <div style={{ marginBottom: '1.25rem', borderRadius: '6px', border: '1px solid #f0c36d', background: '#fffbf0', overflow: 'hidden' }}>
              {nonStockConflictItems.map((item, idx) => (
                <div
                  key={idx}
                  style={{
                    padding: '0.6rem 0.85rem',
                    borderBottom: idx < nonStockConflictItems.length - 1 ? '1px solid #f0c36d' : 'none',
                    display: 'flex',
                    justifyContent: 'space-between',
                    alignItems: 'center',
                    gap: '1rem',
                  }}
                >
                  <span style={{ fontSize: '0.85rem', color: '#333' }}>
                    {item.description?.split('\n')[0] || '(no description)'}
                  </span>
                  {item.amount != null && (
                    <span style={{ fontSize: '0.85rem', color: '#856404', fontWeight: 600, whiteSpace: 'nowrap' }}>
                      £{item.amount.toFixed(2)}
                    </span>
                  )}
                </div>
              ))}
            </div>
            <p style={{ fontSize: '0.8rem', color: '#888', marginBottom: '1.25rem' }}>
              Confirming will include {nonStockConflictItems.length !== 1 ? 'these items' : 'this item'} in the stock cost. Mark {nonStockConflictItems.length !== 1 ? 'them' : 'it'} as non-stock first if that was not intended.
            </p>
            <div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'flex-end' }}>
              <button
                onClick={() => setShowNonStockConfirmModal(false)}
                style={{ ...styles.closeBtn, margin: 0 }}
              >
                Go back
              </button>
              <button
                onClick={async () => {
                  setShowNonStockConfirmModal(false)
                  await doConfirm()
                }}
                style={{
                  padding: '0.5rem 1.25rem',
                  background: '#e85d04',
                  color: 'white',
                  border: 'none',
                  borderRadius: '4px',
                  cursor: 'pointer',
                  fontWeight: 600,
                  fontSize: '0.9rem',
                }}
              >
                Confirm anyway
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Date Picker Modal - select date from parsed dates */}
      {showDatePickerModal && (
        <div style={styles.modalOverlay} onClick={() => setShowDatePickerModal(false)}>
          <div style={{ ...styles.rawOcrModal, maxWidth: '520px' }} onClick={(e) => e.stopPropagation()}>
            <h3>Select Invoice Date</h3>

            {/* OCR image snippet for the InvoiceDate field */}
            {(() => {
              const bbox = getFieldBoundingBox('InvoiceDate')
              if (!bbox) return null
              const isPdf = invoice?.image_path?.toLowerCase().endsWith('.pdf')
              if (isPdf && pdfPages.length > 0) {
                const pageData = pdfPages[bbox.pageNumber - 1]
                if (!pageData) return null
                const bboxX = (bbox.x / 100) * pageData.width
                const bboxY = (bbox.y / 100) * pageData.height
                const bboxW = (bbox.width / 100) * pageData.width
                const bboxH = (bbox.height / 100) * pageData.height
                const pad = 20
                const startX = Math.max(0, bboxX - pad * 3)
                const startY = Math.max(0, bboxY - pad)
                const endX = Math.min(pageData.width, bboxX + bboxW + pad * 3)
                const endY = Math.min(pageData.height, bboxY + bboxH + pad)
                const cropW = endX - startX
                const cropH = endY - startY
                const croppedCanvas = document.createElement('canvas')
                croppedCanvas.width = cropW
                croppedCanvas.height = cropH
                const ctx = croppedCanvas.getContext('2d')
                if (ctx) {
                  ctx.drawImage(pageData.canvas, startX, startY, cropW, cropH, 0, 0, cropW, cropH)
                  // Draw highlight box
                  ctx.strokeStyle = '#e85d04'
                  ctx.lineWidth = 3
                  ctx.strokeRect(bboxX - startX - 4, bboxY - startY - 4, bboxW + 8, bboxH + 8)
                }
                return (
                  <div style={{ marginBottom: '1rem', border: '1px solid #ddd', borderRadius: '6px', overflow: 'hidden' }}>
                    <div style={{ fontSize: '0.75rem', color: '#666', padding: '4px 8px', background: '#f8f9fa', borderBottom: '1px solid #ddd' }}>
                      Date field from document
                    </div>
                    <img src={croppedCanvas.toDataURL()} alt="Date field from invoice" style={{ width: '100%', height: 'auto', display: 'block' }} />
                  </div>
                )
              }
              return null
            })()}

            {/* Suggested fixes when current date is in the future */}
            {invoiceDate && getDateWarning(invoiceDate, 'PENDING') === 'future' && (() => {
              const today = new Date(); today.setHours(0, 0, 0, 0)
              const swapped = getSwappedDate(invoiceDate)
              const swappedIsValid = swapped && new Date(swapped) <= today
              const yearFix = getYearCorrectedDate(invoiceDate)
              if (!swappedIsValid && !yearFix) return null

              const suggestions: { date: string; label: string; sublabel: string }[] = []
              if (yearFix) {
                suggestions.push({
                  date: yearFix.date,
                  label: new Date(yearFix.date).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' }),
                  sublabel: `Year corrected from ${invoiceDate.split('-')[0]} → ${yearFix.year}`,
                })
              }
              if (swappedIsValid && swapped) {
                suggestions.push({
                  date: swapped,
                  label: new Date(swapped).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' }),
                  sublabel: `Day and month swapped from ${new Date(invoiceDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`,
                })
              }

              return (
                <div style={{ marginBottom: '1rem' }}>
                  <div style={{ fontSize: '0.8rem', fontWeight: 600, color: '#e85d04', marginBottom: '0.5rem' }}>
                    ⚠ Suggested fix{suggestions.length > 1 ? 'es' : ''} — date is in the future:
                  </div>
                  {suggestions.map((s, i) => (
                    <div
                      key={i}
                      onClick={() => handleSelectParsedDate(s.date)}
                      style={{
                        padding: '0.75rem 1rem',
                        border: '2px solid #e85d04',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        background: '#fff8f5',
                        transition: 'all 0.2s',
                        marginBottom: i < suggestions.length - 1 ? '0.5rem' : 0,
                      }}
                      onMouseEnter={(e) => { e.currentTarget.style.background = '#fff0e8' }}
                      onMouseLeave={(e) => { e.currentTarget.style.background = '#fff8f5' }}
                    >
                      <div style={{ fontWeight: '600', fontSize: '1.1rem', marginBottom: '0.25rem' }}>{s.label}</div>
                      <div style={{ fontSize: '0.8rem', color: '#888' }}>{s.sublabel}</div>
                    </div>
                  ))}
                  {parsedDates.length > 0 && (
                    <div style={{ borderTop: '1px solid #eee', marginTop: '1rem', paddingTop: '0.75rem', fontSize: '0.8rem', color: '#666', marginBottom: '0.5rem' }}>
                      Or choose a date found in the document:
                    </div>
                  )}
                </div>
              )
            })()}

            {!invoiceDate && (
              <p style={{ color: '#666', marginBottom: '1rem', fontSize: '0.9rem' }}>
                Dates found in the document. Click to select.
              </p>
            )}
            {invoiceDate && getDateWarning(invoiceDate, 'PENDING') !== 'future' && (
              <p style={{ color: '#666', marginBottom: '1rem', fontSize: '0.9rem' }}>
                Dates found in the document. Click to select.
              </p>
            )}

            {parsedDatesLoading ? (
              <p style={{ color: '#999', textAlign: 'center', padding: '2rem' }}>Searching for dates...</p>
            ) : parsedDates.length === 0 ? (
              invoiceDate && getDateWarning(invoiceDate, 'PENDING') === 'future' ? null : (
                <p style={{ color: '#999', textAlign: 'center', padding: '2rem' }}>No dates found in document text.</p>
              )
            ) : (
              <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
                {parsedDates.filter(dateInfo => {
                  // Suppress dates already shown as swap/year-fix suggestions
                  if (!invoiceDate || getDateWarning(invoiceDate, 'PENDING') !== 'future') return true
                  const today = new Date(); today.setHours(0, 0, 0, 0)
                  const swapped = getSwappedDate(invoiceDate)
                  if (swapped && new Date(swapped) <= today && dateInfo.date === swapped) return false
                  const yearFix = getYearCorrectedDate(invoiceDate)
                  if (yearFix && dateInfo.date === yearFix.date) return false
                  return true
                }).map((dateInfo, idx) => {
                  const snippetUrl = dateInfo.bbox ? cropBboxToDataUrl(dateInfo.bbox, { padX: 80, padY: 25, strokeColor: '#e85d04' }) : null
                  return (
                    <div
                      key={idx}
                      onClick={() => handleSelectParsedDate(dateInfo.date)}
                      style={{
                        marginBottom: '0.5rem',
                        border: '1px solid #ddd',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        transition: 'all 0.2s',
                        background: '#fff',
                        overflow: 'hidden',
                      }}
                      onMouseEnter={(e) => {
                        e.currentTarget.style.background = '#f0f7ff'
                        e.currentTarget.style.borderColor = '#007bff'
                      }}
                      onMouseLeave={(e) => {
                        e.currentTarget.style.background = '#fff'
                        e.currentTarget.style.borderColor = '#ddd'
                      }}
                    >
                      {snippetUrl && (
                        <img
                          src={snippetUrl}
                          alt="Date location in document"
                          style={{ width: '100%', height: 'auto', display: 'block', borderBottom: '1px solid #eee' }}
                        />
                      )}
                      <div style={{ padding: '0.75rem 1rem' }}>
                        <div style={{ fontWeight: '600', fontSize: '1.1rem', marginBottom: '0.25rem' }}>
                          {new Date(dateInfo.date).toLocaleDateString('en-GB', {
                            weekday: 'short',
                            day: 'numeric',
                            month: 'short',
                            year: 'numeric'
                          })}
                        </div>
                        <div style={{ fontSize: '0.8rem', color: '#666' }}>
                          Original: {dateInfo.original}
                        </div>
                        <div style={{ fontSize: '0.75rem', color: '#999', marginTop: '0.25rem', fontStyle: 'italic' }}>
                          ...{dateInfo.context}...
                        </div>
                      </div>
                    </div>
                  )
                })}
              </div>
            )}

            <button onClick={() => setShowDatePickerModal(false)} style={styles.closeBtn}>Cancel</button>
          </div>
        </div>
      )}

      {/* Invoice Number Search Modal */}
      {showInvoiceNumberModal && (
        <div style={styles.modalOverlay} onClick={() => setShowInvoiceNumberModal(false)}>
          <div style={{ ...styles.rawOcrModal, maxWidth: '520px' }} onClick={(e) => e.stopPropagation()}>
            <h3>Find Invoice Number</h3>

            {/* OCR image snippet for InvoiceId field if available */}
            {(() => {
              const bbox = getFieldBoundingBox('InvoiceId')
              if (!bbox) return null
              const isPdf = invoice?.image_path?.toLowerCase().endsWith('.pdf')
              if (isPdf && pdfPages.length > 0) {
                const pageData = pdfPages[bbox.pageNumber - 1]
                if (!pageData) return null
                const bboxX = (bbox.x / 100) * pageData.width
                const bboxY = (bbox.y / 100) * pageData.height
                const bboxW = (bbox.width / 100) * pageData.width
                const bboxH = (bbox.height / 100) * pageData.height
                const pad = 20
                const startX = Math.max(0, bboxX - pad * 3)
                const startY = Math.max(0, bboxY - pad)
                const endX = Math.min(pageData.width, bboxX + bboxW + pad * 3)
                const endY = Math.min(pageData.height, bboxY + bboxH + pad)
                const cropW = endX - startX
                const cropH = endY - startY
                const croppedCanvas = document.createElement('canvas')
                croppedCanvas.width = cropW
                croppedCanvas.height = cropH
                const ctx = croppedCanvas.getContext('2d')
                if (ctx) {
                  ctx.drawImage(pageData.canvas, startX, startY, cropW, cropH, 0, 0, cropW, cropH)
                  ctx.strokeStyle = '#007bff'
                  ctx.lineWidth = 3
                  ctx.strokeRect(bboxX - startX - 4, bboxY - startY - 4, bboxW + 8, bboxH + 8)
                }
                return (
                  <div style={{ marginBottom: '1rem', border: '1px solid #ddd', borderRadius: '6px', overflow: 'hidden' }}>
                    <div style={{ fontSize: '0.75rem', color: '#666', padding: '4px 8px', background: '#f8f9fa', borderBottom: '1px solid #ddd' }}>
                      Invoice number field from document
                    </div>
                    <img src={croppedCanvas.toDataURL()} alt="Invoice number field" style={{ width: '100%', height: 'auto', display: 'block' }} />
                  </div>
                )
              }
              return null
            })()}

            {/* Supplier format examples */}
            {invoiceNumberExamples.length > 0 && (
              <div style={{ marginBottom: '1rem', padding: '0.5rem 0.75rem', background: '#f8f9fa', borderRadius: '4px', fontSize: '0.8rem', color: '#555' }}>
                <strong>Format from this supplier:</strong>{' '}
                {invoiceNumberExamples.slice(0, 4).join(', ')}
              </div>
            )}

            <p style={{ color: '#666', marginBottom: '1rem', fontSize: '0.9rem' }}>
              {invoiceNumberLoading
                ? 'Searching document...'
                : invoiceNumberCandidates.length === 0
                ? 'No candidates found in document text.'
                : 'Candidates found in the document. Click to use.'}
            </p>

            {invoiceNumberLoading ? (
              <p style={{ color: '#999', textAlign: 'center', padding: '2rem' }}>Searching for invoice number...</p>
            ) : invoiceNumberCandidates.length === 0 ? null : (
              <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
                {invoiceNumberCandidates.map((c, idx) => {
                  const snippetUrl = c.bbox ? cropBboxToDataUrl(c.bbox, { padX: 80, padY: 25, strokeColor: '#007bff' }) : null
                  return (
                    <div
                      key={idx}
                      onClick={() => handleSelectInvoiceNumber(c.value)}
                      style={{
                        marginBottom: '0.5rem',
                        border: '1px solid #ddd',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        background: '#fff',
                        transition: 'all 0.2s',
                        overflow: 'hidden',
                      }}
                      onMouseEnter={(e) => {
                        e.currentTarget.style.background = '#f0f7ff'
                        e.currentTarget.style.borderColor = '#007bff'
                      }}
                      onMouseLeave={(e) => {
                        e.currentTarget.style.background = '#fff'
                        e.currentTarget.style.borderColor = '#ddd'
                      }}
                    >
                      {snippetUrl && (
                        <img
                          src={snippetUrl}
                          alt="Candidate location in document"
                          style={{ width: '100%', height: 'auto', display: 'block', borderBottom: '1px solid #eee' }}
                        />
                      )}
                      <div style={{ padding: '0.75rem 1rem' }}>
                        <div style={{ fontWeight: '600', fontSize: '1.05rem', marginBottom: '0.25rem', fontFamily: 'monospace' }}>
                          {c.value}
                        </div>
                        <div style={{ fontSize: '0.75rem', color: '#999', marginBottom: '0.2rem' }}>
                          {c.source === 'ocr_field' ? '📋 Azure OCR field'
                            : c.source === 'supplier_pattern' ? '🔗 Matches supplier format'
                            : '🔤 Found near reference keyword'}
                        </div>
                        <div style={{ fontSize: '0.75rem', color: '#888', fontStyle: 'italic' }}>
                          ...{c.context}...
                        </div>
                      </div>
                    </div>
                  )
                })}
              </div>
            )}

            <button onClick={() => setShowInvoiceNumberModal(false)} style={styles.closeBtn}>Cancel</button>
          </div>
        </div>
      )}

      {/* Line Item Search Modal */}
      {showSearchModal && searchingLineItem && (
        <div style={styles.modalOverlay} onClick={() => setShowSearchModal(false)}>
          <div style={styles.searchModal} onClick={(e) => e.stopPropagation()}>
            <h3>Compare Similar Items</h3>

            {/* Current Item - shown at top for comparison */}
            <div style={styles.currentItemSection}>
              <h4 style={styles.currentItemHeader}>Current Item ({suppliers?.find(s => s.id === invoice?.supplier_id)?.name || 'Unknown Supplier'})</h4>
              <table style={styles.searchResultsTable}>
                <thead>
                  <tr>
                    <th style={styles.searchTh}>Description</th>
                    <th style={styles.searchThRight}>Unit Price</th>
                    <th style={styles.searchTh}>Unit</th>
                    <th style={styles.searchTh}>Pack Info</th>
                    <th style={styles.searchTh}>Invoice Date</th>
                  </tr>
                </thead>
                <tbody>
                  <tr style={styles.currentItemRow}>
                    <td style={styles.searchTd}>{searchingLineItem.description}</td>
                    <td style={styles.searchTdRight}>
                      {searchingLineItem.unit_price ? `£${searchingLineItem.unit_price.toFixed(2)}` : '—'}
                    </td>
                    <td style={styles.searchTd}>{searchingLineItem.unit || '—'}</td>
                    <td style={styles.searchTd}>
                      {searchingLineItem.pack_quantity && searchingLineItem.unit_size && searchingLineItem.unit_size_type
                        ? `${searchingLineItem.pack_quantity}x${searchingLineItem.unit_size}${searchingLineItem.unit_size_type}`
                        : searchingLineItem.pack_quantity
                          ? `${searchingLineItem.pack_quantity} pack`
                          : '—'}
                    </td>
                    <td style={styles.searchTd}>{invoice?.invoice_date || '—'}</td>
                  </tr>
                </tbody>
              </table>
            </div>

            <div style={styles.searchInputRow}>
              <input
                type="text"
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                onKeyDown={(e) => e.key === 'Enter' && performSearch(searchQuery)}
                style={styles.searchInput}
                placeholder="Enter search keywords..."
                autoFocus
              />
              <button
                onClick={() => performSearch(searchQuery)}
                style={styles.searchButton}
                disabled={searchLoading}
              >
                {searchLoading ? 'Searching...' : 'Search'}
              </button>
            </div>

            {searchResults && (
              <div style={styles.searchResultsContainer}>
                <p style={styles.resultsInfo}>
                  Found {searchResults.total_matches} matches
                  {searchResults.extracted_keywords && (
                    <span style={{ color: '#666' }}> (searched: "{searchResults.extracted_keywords}")</span>
                  )}
                </p>

                {searchResults.results.length === 0 ? (
                  <p style={{ color: '#999', fontStyle: 'italic' }}>No matching items found.</p>
                ) : (
                  searchResults.results.map((group) => (
                    <div key={group.supplier_id || 'unknown'} style={styles.supplierGroup}>
                      <h4 style={styles.supplierHeader}>{group.supplier_name}</h4>
                      <table style={styles.searchResultsTable}>
                        <thead>
                          <tr>
                            <th style={styles.searchTh}>Description</th>
                            <th style={styles.searchThRight}>Unit Price</th>
                            <th style={styles.searchTh}>Unit</th>
                            <th style={styles.searchTh}>Pack Info</th>
                            <th style={styles.searchTh}>Last Invoice</th>
                            <th style={styles.searchTh}></th>
                          </tr>
                        </thead>
                        <tbody>
                          {group.items.map((item, idx) => (
                            <tr key={idx}>
                              <td style={styles.searchTd}>{item.description}</td>
                              <td style={styles.searchTdRight}>
                                {item.unit_price != null ? `£${item.unit_price.toFixed(2)}` : '—'}
                              </td>
                              <td style={styles.searchTd}>{item.unit || '—'}</td>
                              <td style={styles.searchTd}>{item.pack_info || '—'}</td>
                              <td style={styles.searchTd}>{item.last_invoice_date || '—'}</td>
                              <td style={styles.searchTd}>
                                <a
                                  href={`/kitchen/invoice/${item.invoice_id}`}
                                  target="_blank"
                                  rel="noopener noreferrer"
                                  style={styles.invoiceLink}
                                  title="Open invoice in new tab"
                                >
                                  View
                                </a>
                              </td>
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    </div>
                  ))
                )}
              </div>
            )}

            <button onClick={() => setShowSearchModal(false)} style={styles.closeBtn}>Close</button>
          </div>
        </div>
      )}

      {/* Bulk Stock Modal */}
      {showBulkStockModal && (
        <div style={styles.modalOverlay} onClick={() => setShowBulkStockModal(false)}>
          <div style={{ ...styles.modal, maxWidth: '400px', textAlign: 'center' }} onClick={(e) => e.stopPropagation()}>
            <h3 style={{ marginBottom: '1rem' }}>Bulk Stock Update</h3>
            <p style={{ marginBottom: '1.5rem', color: '#666' }}>
              Mark all line items on this invoice as:
            </p>
            <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
              <button
                onClick={() => handleBulkStockUpdate(true)}
                style={{
                  ...styles.saveBtn,
                  padding: '1rem',
                  background: '#28a745',
                  fontSize: '0.95rem'
                }}
              >
                Mark All as Stock
              </button>
              <button
                onClick={() => handleBulkStockUpdate(false)}
                style={{
                  ...styles.saveBtn,
                  padding: '1rem',
                  background: '#dc3545',
                  fontSize: '0.95rem'
                }}
              >
                Mark All as Non-Stock
              </button>
              <button
                onClick={() => setShowBulkStockModal(false)}
                style={{
                  ...styles.backBtn,
                  padding: '0.75rem',
                  marginTop: '0.5rem'
                }}
              >
                Cancel
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Price History Modal */}
      {historyModal && (
        <LineItemHistoryModal
          isOpen={historyModal.isOpen}
          onClose={() => setHistoryModal(null)}
          productCode={historyModal.productCode}
          description={historyModal.description}
          unit={historyModal.unit}
          supplierId={historyModal.supplierId}
          supplierName={historyModal.supplierName}
          currentPrice={historyModal.currentPrice}
          sourceInvoiceId={historyModal.sourceInvoiceId}
          sourceLineItemId={historyModal.sourceLineItemId}
          onAcknowledge={() => {
            refetchLineItems()
          }}
        />
      )}

      {/* Create Dispute Modal */}
      {showDisputeModal && invoice && (
        <CreateDisputeModal
          invoiceId={invoice.id}
          invoiceNumber={invoice.invoice_number}
          supplierId={invoice.supplier_id || null}
          supplierName={invoice.supplier_name || null}
          lineItems={lineItems || []}
          onClose={() => setShowDisputeModal(false)}
          onSuccess={() => {
            refetchInvoice() // Refresh invoice to update dispute count
          }}
        />
      )}

      {/* Link Credit Note to Dispute Modal */}
      {showLinkDisputeModal && invoice && invoice.supplier_id && (
        <LinkDisputeModal
          supplierId={invoice.supplier_id}
          supplierName={invoice.supplier_name || null}
          creditNoteInvoiceId={invoice.id}
          creditNoteNumber={invoice.invoice_number}
          creditNoteAmount={invoice.total}
          onClose={() => setShowLinkDisputeModal(false)}
          onSuccess={() => {
            setShowLinkDisputeModal(false)
            refetchInvoice() // Refresh to show "Resolved Dispute" button
          }}
        />
      )}

      {/* View Dispute Detail Modal */}
      {viewingDisputeId && (
        <DisputeDetailModal
          disputeId={viewingDisputeId}
          onClose={() => setViewingDisputeId(null)}
          onUpdate={() => {
            refetchInvoice() // Refresh invoice to update dispute data
          }}
        />
      )}

      {/* Description Swap Modal */}
      {descSwapItem && (
        <div style={styles.modalOverlay} onClick={() => setDescSwapItem(null)}>
          <div style={styles.descSwapModal} onClick={(e) => e.stopPropagation()}>
            <h3 style={{ marginBottom: '0.5rem' }}>Choose Description</h3>
            <p style={styles.modalHint}>
              OCR extracted two different descriptions. Select the correct one:
            </p>

            <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginTop: '1rem' }}>
              {/* Current Description */}
              <div
                style={styles.descriptionOption}
                onClick={() => {
                  // Keep current - just close
                  setDescSwapItem(null)
                }}
              >
                <div style={{ fontWeight: 'bold', marginBottom: '0.5rem', color: '#666' }}>
                  Current (Value)
                </div>
                <div style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace', fontSize: '0.85rem' }}>
                  {descSwapItem.description || '(empty)'}
                </div>
              </div>

              {/* Alternative Description */}
              <div
                style={{ ...styles.descriptionOption, borderColor: '#e94560' }}
                onClick={async () => {
                  // Update to alternative
                  await updateLineItemMutation.mutateAsync({
                    itemId: descSwapItem.id,
                    data: {
                      description: descSwapItem.description_alt,
                      description_alt: null  // Clear alt after swap
                    }
                  })
                  setDescSwapItem(null)
                }}
              >
                <div style={{ fontWeight: 'bold', marginBottom: '0.5rem', color: '#e94560' }}>
                  Alternative (Content) ✓ Select
                </div>
                <div style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace', fontSize: '0.85rem' }}>
                  {descSwapItem.description_alt || '(empty)'}
                </div>
              </div>
            </div>

            <button
              onClick={() => setDescSwapItem(null)}
              style={{ ...styles.backBtn, marginTop: '1rem', padding: '0.75rem' }}
            >
              Cancel
            </button>
          </div>
        </div>
      )}

      {/* PO View/Edit Modal */}
      <PurchaseOrderModal
        isOpen={viewingPoId !== null}
        onClose={() => setViewingPoId(null)}
        onSaved={() => refetchPoMatch()}
        poId={viewingPoId}
      />

      {/* Ingredient Mapping Modal */}
      {ingredientModalItem && (
        <div style={styles.modalOverlay} onClick={closeIngredientModal}>
          <div style={{ ...styles.wideModal, maxWidth: '650px' }} onClick={(e) => e.stopPropagation()}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
              <h3 style={{ margin: 0 }}>Ingredient Mapping</h3>
              <button onClick={closeIngredientModal} style={{ background: 'none', border: 'none', fontSize: '1.5rem', cursor: 'pointer', color: '#666' }}>&times;</button>
            </div>

            {/* Line item context */}
            <div style={{ padding: '0.75rem', background: '#f8f9fa', borderRadius: '6px', marginBottom: '1rem', fontSize: '0.9rem' }}>
              <strong>{ingredientModalItem.description || ingredientModalItem.product_code || 'Unknown item'}</strong>
              {ingredientModalItem.unit_price && <span style={{ marginLeft: '1rem', color: '#666' }}>£{ingredientModalItem.unit_price.toFixed(2)}</span>}
              {ingredientModalItem.raw_content && (
                <div style={{ fontSize: '0.8rem', color: '#999', marginTop: '0.25rem' }}>Raw: {ingredientModalItem.raw_content.substring(0, 80)}</div>
              )}
            </div>

            {/* Ingredient mapping section */}
            <div style={{ marginBottom: '1rem' }}>
              <label style={{ fontWeight: '600', fontSize: '0.9rem', display: 'block', marginBottom: '0.5rem' }}>
                Ingredient {selectedIngredientId && <span style={{ color: '#28a745', fontWeight: 'normal' }}>({selectedIngredientName})</span>}
              </label>

              {selectedIngredientId ? (
                <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem', background: '#d4edda', borderRadius: '6px', border: '1px solid #c3e6cb' }}>
                  <span style={{ flex: 1 }}>{selectedIngredientName} <span style={{ color: '#666', fontSize: '0.85rem' }}>({selectedIngredientUnit})</span></span>
                  <button
                    onClick={() => { setSelectedIngredientId(null); setSelectedIngredientName(''); setSelectedIngredientUnit(''); setIngredientConversionDisplay(''); }}
                    style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#666', fontSize: '1.1rem' }}
                  >&times;</button>
                </div>
              ) : (
                <>
                  <div style={{ position: 'relative' }}>
                    <input
                      type="text"
                      value={ingredientSearch}
                      onChange={(e) => {
                        setIngredientSearch(e.target.value)
                        searchIngredients(e.target.value)
                      }}
                      placeholder="Search ingredients..."
                      style={{ ...styles.input, width: '100%', boxSizing: 'border-box' }}
                    />
                    {ingredientSearchLoading && <span style={{ position: 'absolute', right: '10px', top: '10px', color: '#999' }}>...</span>}
                  </div>

                  {/* Suggestions dropdown */}
                  {ingredientSuggestions.length > 0 && (
                    <div style={{ border: '1px solid #ddd', borderRadius: '0 0 6px 6px', maxHeight: '200px', overflowY: 'auto', background: '#fff' }}>
                      {ingredientSuggestions.map((s) => (
                        <div
                          key={s.id}
                          onClick={() => selectIngredient(s)}
                          style={{ padding: '0.5rem 0.75rem', cursor: 'pointer', borderBottom: '1px solid #f0f0f0', fontSize: '0.9rem', display: 'flex', justifyContent: 'space-between' }}
                          onMouseOver={(e) => (e.currentTarget.style.background = '#f0f7ff')}
                          onMouseOut={(e) => (e.currentTarget.style.background = '#fff')}
                        >
                          <span>{s.name} <span style={{ color: '#999', fontSize: '0.8rem' }}>({s.standard_unit})</span></span>
                          <span style={{ color: '#999', fontSize: '0.8rem' }}>{s.category_name || ''} {s.similarity ? `${(s.similarity * 100).toFixed(0)}%` : ''}</span>
                        </div>
                      ))}
                    </div>
                  )}

                  {/* LLM FEATURE — AI Match button + results — see LLM-MANIFEST.md for removal instructions */}
                  {settings?.llm_enabled && settings?.anthropic_api_key_set && ingredientModalItem?.description && !selectedIngredientId && (
                    <div style={{ marginTop: '0.5rem' }}>
                      <button
                        onClick={() => handleAiMatch(ingredientModalItem!.description!)}
                        disabled={aiMatchLoading}
                        style={{ padding: '0.4rem 0.75rem', background: '#7952b3', color: 'white', border: 'none', borderRadius: '4px', cursor: aiMatchLoading ? 'wait' : 'pointer', fontSize: '0.85rem', opacity: aiMatchLoading ? 0.7 : 1 }}
                      >
                        {aiMatchLoading ? 'Matching...' : '\u2728 AI Match'}
                      </button>
                      {aiMatchResults.length > 0 && (
                        <div style={{ border: '1px solid #d4c5f9', borderRadius: '6px', marginTop: '0.35rem', maxHeight: '160px', overflowY: 'auto', background: '#f8f5ff' }}>
                          {aiMatchResults.map((m) => (
                            <div
                              key={m.id}
                              onClick={() => {
                                const found = ingredientSuggestions.find(s => s.id === m.id)
                                if (found) selectIngredient(found)
                                else { setSelectedIngredientId(m.id); setSelectedIngredientName(m.name); setSelectedIngredientUnit(''); }
                                setAiMatchResults([])
                              }}
                              style={{ padding: '0.4rem 0.75rem', cursor: 'pointer', borderBottom: '1px solid #e8e0f7', fontSize: '0.85rem', display: 'flex', justifyContent: 'space-between' }}
                              onMouseOver={(e) => (e.currentTarget.style.background = '#ede5ff')}
                              onMouseOut={(e) => (e.currentTarget.style.background = '#f8f5ff')}
                            >
                              <span>{'\u2728'} {m.name}</span>
                              <span style={{ color: '#7952b3', fontSize: '0.8rem' }}>{(m.confidence * 100).toFixed(0)}%</span>
                            </div>
                          ))}
                        </div>
                      )}
                    </div>
                  )}

                  {/* Create new ingredient */}
                  <button
                    onClick={() => setShowCreateIngredient(true)}
                    style={{ marginTop: '0.5rem', padding: '0.4rem 0.75rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '0.85rem' }}
                  >
                    + Create new ingredient
                  </button>
                </>
              )}
            </div>

            {/* Invoice → Ingredient conversion */}
            <div style={{ marginBottom: '1rem' }}>
              <label style={{ fontWeight: '600', fontSize: '0.9rem', display: 'block', marginBottom: '0.25rem' }}>
                {selectedIngredientId ? `How much ${selectedIngredientName} is in this line item?` : 'Unit size & pricing'}
              </label>
              <div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '0.5rem' }}>
                {ingredientModalItem?.description}{ingredientModalItem?.unit_price ? ` @ £${ingredientModalItem.unit_price.toFixed(2)}` : ''}
                {ingredientModalItem?.quantity && ingredientModalItem.quantity > 1 ? ` × ${ingredientModalItem.quantity}` : ''}
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', alignItems: 'flex-end' }}>
                <div style={styles.costBreakdownField}>
                  <label style={{ fontSize: '0.8rem', color: '#666' }}>
                    Contains
                    {/* LLM FEATURE */}
                    {aiPackSizeLoading && <span style={{ marginLeft: '4px', fontSize: '0.7rem', color: '#7952b3' }}>analysing...</span>}
                    {aiPackSizeSource === 'ai' && <span style={{ marginLeft: '4px', fontSize: '0.7rem', color: '#7952b3' }} title="Deduced by AI from product knowledge">{'\u2728'}</span>}
                    {aiPackSizeSource === 'regex' && <span style={{ marginLeft: '4px', fontSize: '0.7rem', color: '#28a745' }} title="Parsed from description">auto</span>}
                    {aiPackSizeSource === 'unit_field' && <span style={{ marginLeft: '4px', fontSize: '0.7rem', color: '#28a745' }} title="Inferred from invoice unit field">auto</span>}
                  </label>
                  <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
                    <input
                      type="number" step="0.1"
                      value={costBreakdownEdits.unit_size || ''}
                      onChange={(e) => { const v = { ...costBreakdownEdits, unit_size: parseFloat(e.target.value) || null }; setCostBreakdownEdits(v); updateConversionDisplay(undefined, v) }}
                      onFocus={(e) => e.target.select()}
                      style={{ ...styles.costBreakdownInput, width: '70px' }}
                      placeholder="e.g., 2"
                    />
                    <select
                      value={costBreakdownEdits.unit_size_type || selectedIngredientUnit || ''}
                      onChange={(e) => { const v = { ...costBreakdownEdits, unit_size_type: e.target.value || null }; setCostBreakdownEdits(v); updateConversionDisplay(undefined, v) }}
                      style={{ ...styles.costBreakdownInput, width: '60px' }}
                    >
                      <option value="">--</option>
                      <option value="each">each</option>
                      <option value="g">g</option>
                      <option value="kg">kg</option>
                      <option value="ml">ml</option>
                      <option value="ltr">ltr</option>
                      <option value="oz">oz</option>
                      <option value="cl">cl</option>
                    </select>
                    {selectedIngredientUnit && costBreakdownEdits.unit_size_type && costBreakdownEdits.unit_size_type !== selectedIngredientUnit && (
                      <span style={{ fontSize: '0.75rem', color: '#999' }}>→ {selectedIngredientUnit}</span>
                    )}
                  </div>
                </div>
                <div style={styles.costBreakdownField}>
                  <label style={{ fontSize: '0.8rem', color: '#666' }}>Pack of</label>
                  <input
                    type="number"
                    value={costBreakdownEdits.pack_quantity || ''}
                    onChange={(e) => { const v = { ...costBreakdownEdits, pack_quantity: parseInt(e.target.value) || null }; setCostBreakdownEdits(v); updateConversionDisplay(undefined, v) }}
                    onFocus={(e) => e.target.select()}
                    style={{ ...styles.costBreakdownInput, width: '50px' }}
                    placeholder="1"
                  />
                </div>
                <div style={styles.costBreakdownField}>
                  <label style={{ fontSize: '0.8rem', color: '#666' }}>Line Price</label>
                  <span style={{ ...styles.costBreakdownValue, fontSize: '0.9rem' }}>
                    {costBreakdownEdits.unit_price ? `£${costBreakdownEdits.unit_price.toFixed(2)}` : '--'}
                  </span>
                </div>
              </div>
              {ingredientConversionDisplay && (
                <div style={{ marginTop: '0.5rem', padding: '0.5rem 0.75rem', background: '#e8f5e9', borderRadius: '6px', fontSize: '0.9rem', color: '#2e7d32', fontWeight: '500' }}>
                  {ingredientConversionDisplay}
                </div>
              )}
            </div>

            {/* Legacy portioning (collapsible) */}
            <div style={{ marginBottom: '1rem' }}>
              <button
                onClick={() => setShowLegacyPortioning(!showLegacyPortioning)}
                style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.85rem', color: '#666', padding: 0 }}
              >
                {showLegacyPortioning ? '▾' : '▸'} Legacy Portioning
              </button>
              {showLegacyPortioning && (
                <div style={{ marginTop: '0.5rem', padding: '0.75rem', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #e0e0e0' }}>
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
                    <div style={styles.costBreakdownField}>
                      <label style={{ fontSize: '0.8rem', color: '#666' }}>Portions/Unit</label>
                      <input
                        type="number"
                        value={costBreakdownEdits.portions_per_unit ?? ''}
                        onChange={(e) => setCostBreakdownEdits({ ...costBreakdownEdits, portions_per_unit: e.target.value ? parseInt(e.target.value) : null })}
                        style={styles.costBreakdownInput} min="1" placeholder="--"
                      />
                    </div>
                    <div style={{ ...styles.costBreakdownField, minWidth: '150px' }}>
                      <label style={{ fontSize: '0.8rem', color: '#666' }}>Portion Desc</label>
                      <input
                        type="text" value={portionDescription}
                        onChange={(e) => setPortionDescription(e.target.value)}
                        style={{ ...styles.costBreakdownInput, width: '100%' }}
                        placeholder="e.g., 250ml glass"
                      />
                    </div>
                    <div style={styles.costBreakdownField}>
                      <label style={{ fontSize: '0.8rem', color: '#666' }}>Cost/Portion</label>
                      <span style={{ ...styles.costBreakdownValue, color: '#28a745', fontWeight: '600', fontSize: '0.9rem' }}>
                        {costBreakdownEdits.pack_quantity && costBreakdownEdits.unit_price && costBreakdownEdits.portions_per_unit
                          ? `£${(costBreakdownEdits.unit_price / (costBreakdownEdits.pack_quantity * costBreakdownEdits.portions_per_unit)).toFixed(4)}`
                          : '--'}
                      </span>
                    </div>
                  </div>
                </div>
              )}
            </div>

            {/* Save as default */}
            {(ingredientModalItem.product_code || ingredientModalItem.description) && (
              <>
                {definitionLoading ? (
                  <div style={{ fontSize: '0.85rem', color: '#666', marginBottom: '0.5rem' }}>Loading saved default...</div>
                ) : currentDefinition ? (
                  <div style={{ ...styles.savedByInfo, marginBottom: '0.5rem' }}>
                    <span style={{ fontSize: '0.85rem' }}>
                      Definition by <strong>{currentDefinition.saved_by_username || 'Unknown'}</strong>
                      {currentDefinition.updated_at && <> on {new Date(currentDefinition.updated_at).toLocaleDateString()}</>}
                    </span>
                  </div>
                ) : null}
                {!selectedIngredientId && (
                  <div style={{ ...styles.saveAsDefaultRow, marginBottom: '1rem' }}>
                    <label style={styles.saveAsDefaultLabel}>
                      <input type="checkbox" checked={saveAsDefault} onChange={(e) => setSaveAsDefault(e.target.checked)} style={{ marginRight: '0.5rem' }} />
                      {currentDefinition ? 'Update saved default' : `Save as default for "${ingredientModalItem.product_code || ingredientModalItem.description?.substring(0, 30)}"`}
                    </label>
                  </div>
                )}
              </>
            )}

            {/* Actions */}
            <div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'flex-end' }}>
              <button onClick={closeIngredientModal} style={{ ...styles.smallBtnCancel, padding: '0.5rem 1.25rem' }}>Cancel</button>
              <button onClick={() => saveCostBreakdown(ingredientModalItem.id)} style={{ ...styles.smallBtn, padding: '0.5rem 1.25rem' }}>
                {selectedIngredientId ? 'Save & Map Ingredient' : (saveAsDefault ? 'Save & Update Default' : 'Save')}
              </button>
            </div>
          </div>
        </div>
      )}

      <IngredientModal
        open={showCreateIngredient}
        onClose={() => setShowCreateIngredient(false)}
        onSaved={handleIngredientCreated}
        prePopulateName={ingredientSearch || ingredientModalItem?.description?.split('\n')[0] || ''}
        preSelectLineItem={ingredientModalItem ? {
          product_code: ingredientModalItem.product_code,
          description: ingredientModalItem.description?.split('\n')[0] || '',
          supplier_id: invoice?.supplier_id || null,
          supplier_name: invoice?.supplier_name || null,
          unit: ingredientModalItem.unit,
          most_recent_price: ingredientModalItem.unit_price,
          total_quantity: ingredientModalItem.quantity,
          occurrence_count: 1,
          most_recent_invoice_id: Number(id),
          most_recent_line_number: ingredientModalItem.line_number,
          most_recent_pack_quantity: ingredientModalItem.pack_quantity,
          most_recent_unit_size: ingredientModalItem.unit_size,
          most_recent_unit_size_type: ingredientModalItem.unit_size_type,
          ingredient_id: ingredientModalItem.ingredient_id || null,
          ingredient_name: ingredientModalItem.ingredient_name || null,
        } as LineItemResult : null}
      />
    </div>
  )
}

const styles: Record<string, React.CSSProperties> = {
  loading: { padding: '2rem', textAlign: 'center', color: '#666' },
  error: { padding: '2rem', textAlign: 'center', color: '#c00' },
  pageContainer: { display: 'flex', flexDirection: 'column', gap: '1.5rem' },
  topRow: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' },
  imageSection: { background: 'white', padding: '1.5rem', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', overflow: 'hidden', minWidth: 0 },
  imageZoomControls: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '1rem', justifyContent: 'center' },
  zoomBtn: { padding: '0.5rem 0.75rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '6px', cursor: 'pointer', fontSize: '1rem', fontWeight: 'bold', minWidth: '36px' },
  zoomLabel: { fontSize: '0.9rem', color: '#666', minWidth: '50px', textAlign: 'center' },
  zoomResetBtnSmall: { padding: '0.4rem 0.75rem', background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem', fontWeight: '500' },
  imageScrollContainer: { marginTop: '0.5rem', maxHeight: '70vh', maxWidth: '100%', overflow: 'auto', background: '#f5f5f5', borderRadius: '8px', padding: '8px' },
  imageContainer: { position: 'relative' },
  image: { width: '100%', borderRadius: '8px', display: 'block' },
  pdfScrollContainer: { marginTop: '1rem', maxHeight: '70vh', overflow: 'auto', background: '#e5e5e5', borderRadius: '8px', padding: '16px' },
  pdfPageWrapper: { marginBottom: '16px' },
  pdfPageContainer: { position: 'relative', margin: '0 auto', boxShadow: '0 2px 8px rgba(0,0,0,0.15)' },
  pdfPageImage: { width: '100%', height: '100%', display: 'block' },
  pageLabel: { textAlign: 'center', fontSize: '0.8rem', color: '#666', marginTop: '4px' },
  pdfCanvas: { width: '100%', borderRadius: '8px', display: 'block' },
  highlightOverlay: { position: 'absolute', background: 'rgba(255, 200, 0, 0.3)', borderRadius: '2px', cursor: 'pointer', boxShadow: '0 0 8px rgba(255, 200, 0, 0.6)', animation: 'pulse 1.5s infinite' },
  imagePlaceholder: { width: '100%', height: '300px', background: '#f5f5f5', borderRadius: '8px', marginTop: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' },
  pdfViewer: { width: '100%', height: '500px', borderRadius: '8px', border: 'none' },
  openPdfLink: { display: 'block', marginTop: '0.5rem', textAlign: 'center', color: '#0066cc', fontSize: '0.85rem' },
  zoomResetBtn: { display: 'block', margin: '0.5rem auto', padding: '0.4rem 1rem', background: '#fff3cd', border: '1px solid #ffc107', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem', fontWeight: '500' },
  confidenceBadge: { marginTop: '1rem', padding: '0.5rem 1rem', background: '#f0f0f0', borderRadius: '20px', textAlign: 'center', fontSize: '0.9rem', color: '#666' },
  formSection: { background: 'white', padding: '1.5rem', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' },
  duplicateWarning: { padding: '1rem', borderRadius: '8px', marginBottom: '1rem', cursor: 'pointer', borderWidth: '1px', borderStyle: 'solid', fontWeight: '500' },
  form: { display: 'flex', flexDirection: 'column', gap: '0.75rem' },
  row: { display: 'flex', gap: '1rem' },
  label: { display: 'flex', flexDirection: 'column', gap: '0.25rem', color: '#333', fontWeight: '500', fontSize: '0.9rem' },
  input: { padding: '0.5rem', borderRadius: '6px', border: '1px solid #ddd', fontSize: '0.95rem', flex: 1 },
  inputWithEye: { display: 'flex', gap: '0.5rem', alignItems: 'center' },
  eyeBtn: { padding: '0.4rem 0.5rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '6px', cursor: 'pointer', fontSize: '0.9rem', lineHeight: 1, flexShrink: 0 },
  eyeBtnActive: { background: '#fff3cd', border: '1px solid #ffc107' },
  extractedHint: { fontSize: '0.8rem', color: '#666', fontWeight: 'normal', fontStyle: 'italic' },
  extractedHintRow: { display: 'flex', alignItems: 'center', gap: '0.75rem', marginTop: '0.25rem', flexWrap: 'wrap' },
  fuzzyMatchWarning: { fontSize: '0.8rem', color: '#856404', background: '#fff3cd', padding: '0.35rem 0.5rem', borderRadius: '4px', marginTop: '0.25rem', border: '1px solid #ffc107', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem', flexWrap: 'wrap' },
  addAliasBtn: { padding: '0.2rem 0.5rem', background: '#28a745', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '0.75rem', fontWeight: '500', whiteSpace: 'nowrap' },
  supplierRow: { display: 'flex', gap: '0.5rem', alignItems: 'center' },
  createSupplierBtn: { padding: '0.5rem 0.75rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem', whiteSpace: 'nowrap' },
  createFromExtractedBtn: { padding: '0.25rem 0.5rem', background: '#e94560', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '0.75rem', fontWeight: 'normal' },
  modalLabel: { display: 'flex', flexDirection: 'column', gap: '0.5rem', color: '#333', fontWeight: '500', marginTop: '1rem' },
  lineItemsSection: { background: 'white', padding: '1.5rem', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' },
  lineItemsTableContainer: { maxHeight: '60vh', overflowY: 'auto', overflowX: 'auto', border: '1px solid #eee', borderRadius: '8px', position: 'relative' } as React.CSSProperties,
  stickyThead: { position: 'sticky', top: 0, zIndex: 2, borderBottom: '2px solid #ddd' } as React.CSSProperties,
  stickyPageIndicatorCell: { background: '#e9ecef', padding: '0.3rem 1rem', textAlign: 'center', fontSize: '0.85rem', fontWeight: '500', color: '#495057' } as React.CSSProperties,
  lineItemsTable: { width: '100%', borderCollapse: 'collapse', fontSize: '0.95rem' },
  th: { textAlign: 'left', padding: '0.75rem 0.5rem', fontWeight: '600', background: '#f8f9fa' } as React.CSSProperties,
  pageBreakRow: { background: '#f8f9fa' },
  pageBreakCell: { textAlign: 'center', padding: '0.5rem', color: '#6c757d', fontSize: '0.85rem', fontStyle: 'italic', borderBottom: '1px dashed #dee2e6', borderTop: '1px dashed #dee2e6' } as React.CSSProperties,
  td: { padding: '0.75rem 0.5rem', borderBottom: '1px solid #eee', verticalAlign: 'middle' },
  tableInput: { padding: '0.35rem', borderRadius: '4px', border: '1px solid #ddd', fontSize: '0.9rem', width: '100%' },
  smallBtn: { padding: '0.35rem 0.75rem', background: '#5cb85c', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', marginRight: '0.25rem', fontSize: '0.8rem' },
  smallBtnCancel: { padding: '0.35rem 0.75rem', background: '#999', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8rem' },
  editBtn: { padding: '0.35rem 0.75rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '0.8rem' },
  iconBtn: { padding: '0.35rem 0.5rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '1rem', display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: '32px', height: '32px' },
  noItems: { color: '#999', fontStyle: 'italic', marginTop: '0.5rem' },
  status: { marginTop: '1rem', padding: '0.75rem', background: '#f5f5f5', borderRadius: '6px', color: '#666', fontSize: '0.9rem' },
  docTypeBadge: { marginLeft: '1rem', padding: '0.25rem 0.5rem', background: '#17a2b8', color: 'white', borderRadius: '4px', fontSize: '0.75rem' },
  actions: { display: 'flex', gap: '0.75rem', marginTop: '1rem' },
  saveBtn: { flex: 1, padding: '1.25rem', background: '#1a1a2e', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' },
  confirmBtn: { flex: 1, padding: '1.25rem', background: '#5cb85c', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem' },
  secondaryActions: { display: 'flex', gap: '0.75rem', marginTop: '0.75rem' },
  deleteBtn: { flex: 1, padding: '0.5rem', background: '#dc3545', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem' },
  rawOcrBtn: { flex: 1, padding: '0.5rem', background: '#6c757d', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem' },
  backBtn: { marginTop: '0.5rem', padding: '0.5rem', background: 'transparent', border: 'none', color: '#666', cursor: 'pointer', width: '100%', fontSize: '0.9rem' },
  modalOverlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 },
  modal: { background: 'white', padding: '2rem', borderRadius: '12px', maxWidth: '400px', width: '90%' },
  wideModal: { background: 'white', padding: '2rem', borderRadius: '12px', maxWidth: '800px', width: '90%', maxHeight: '80vh', overflowY: 'auto' },
  modalInfo: { margin: '1rem 0', padding: '1rem', background: '#f5f5f5', borderRadius: '8px' },
  modalDuplicateInfo: { padding: '0.75rem', background: '#d4edda', borderRadius: '6px', marginBottom: '1rem', color: '#155724', fontSize: '0.9rem' },
  modalActions: { display: 'flex', gap: '1rem', marginTop: '1.5rem' },
  cancelBtn: { flex: 1, padding: '0.75rem', background: '#f0f0f0', border: 'none', borderRadius: '6px', cursor: 'pointer' },
  confirmDeleteBtn: { flex: 1, padding: '0.75rem', background: '#dc3545', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold' },
  compareGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginTop: '1rem' },
  compareCard: { padding: '1rem', border: '2px solid #ddd', borderRadius: '8px' },
  viewBtn: { marginTop: '0.5rem', padding: '0.5rem', background: '#1a1a2e', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', width: '100%', fontSize: '0.85rem' },
  closeBtn: { marginTop: '1.5rem', padding: '0.75rem 2rem', background: '#1a1a2e', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' },
  rawOcrModal: { background: 'white', padding: '2rem', borderRadius: '12px', maxWidth: '900px', width: '95%', maxHeight: '85vh', overflowY: 'auto' },
  rawOcrHint: { color: '#666', fontSize: '0.9rem', marginBottom: '1rem' },
  rawOcrContent: { marginTop: '1rem' },
  rawOcrDoc: { marginBottom: '1.5rem', padding: '1rem', background: '#f8f9fa', borderRadius: '8px' },
  rawOcrTable: { width: '100%', borderCollapse: 'collapse', marginTop: '0.5rem', fontSize: '0.85rem' },
  rawOcrTh: { textAlign: 'left', padding: '0.5rem', borderBottom: '2px solid #ddd', fontWeight: '600', background: '#e9ecef' },
  rawOcrTd: { padding: '0.5rem', borderBottom: '1px solid #eee', verticalAlign: 'top', wordBreak: 'break-word', maxWidth: '300px' },
  lineItemPreviewCell: { padding: '0.5rem', background: '#f8f9fa', borderBottom: '1px solid #eee', textAlign: 'center' },
  lineItemPreviewContainer: { width: '100%', overflow: 'hidden', display: 'inline-block' },
  lineItemPreviewCrop: { width: '100%', backgroundRepeat: 'no-repeat' },
  scaleBtn: { padding: '0.25rem', background: 'transparent', borderWidth: '1px', borderStyle: 'solid', borderColor: '#ddd', borderRadius: '4px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#666' },
  scaleBtnActive: { background: '#e8f5e9', borderColor: '#28a745' },
  costBreakdownCell: { padding: '0', background: '#f8f9fa', borderBottom: '1px solid #ddd' },
  costBreakdownContainer: { padding: '1rem', margin: '0.5rem', background: '#fff', borderRadius: '8px', border: '1px solid #e0e0e0', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' },
  costBreakdownHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', paddingBottom: '0.5rem', borderBottom: '1px solid #eee' },
  rawContentHint: { fontSize: '0.75rem', color: '#999', fontStyle: 'italic', maxWidth: '400px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  costBreakdownGrid: { display: 'flex', flexWrap: 'wrap', gap: '1rem', marginBottom: '1rem' },
  costBreakdownField: { display: 'flex', flexDirection: 'column', gap: '0.25rem', minWidth: '70px' },
  costBreakdownInput: { padding: '0.4rem', borderRadius: '4px', border: '1px solid #ddd', fontSize: '0.9rem', width: '70px', MozAppearance: 'textfield', appearance: 'textfield' } as React.CSSProperties,
  costBreakdownValue: { fontSize: '0.95rem', padding: '0.4rem 0' },
  costBreakdownActions: { display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', paddingTop: '0.5rem', borderTop: '1px solid #eee' },
  saveAsDefaultRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0.75rem', marginBottom: '0.5rem', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #e0e0e0' },
  saveAsDefaultLabel: { display: 'flex', alignItems: 'center', fontSize: '0.9rem', cursor: 'pointer', fontWeight: '500' },
  saveAsDefaultHint: { fontSize: '0.75rem', color: '#666', fontStyle: 'italic' },
  savedByInfo: { display: 'flex', alignItems: 'center', padding: '0.6rem 0.75rem', marginBottom: '0.5rem', background: '#e8f4fd', borderRadius: '6px', border: '1px solid #b8daff', fontSize: '0.85rem', color: '#004085' },
  // Search modal styles
  searchBtn: { padding: '0.25rem 0.5rem', background: '#f0f0f0', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '0.85rem', marginRight: '0.25rem' },
  searchModal: { background: 'white', padding: '2rem', borderRadius: '12px', maxWidth: '900px', width: '95%', maxHeight: '85vh', overflowY: 'auto' },
  searchContext: { color: '#666', marginBottom: '1rem', fontSize: '0.9rem' },
  searchInputRow: { display: 'flex', gap: '0.5rem', marginBottom: '1rem' },
  searchInput: { flex: 1, padding: '0.75rem', borderRadius: '6px', border: '1px solid #ddd', fontSize: '1rem' },
  searchButton: { padding: '0.75rem 1.5rem', background: '#1a1a2e', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: '500' },
  searchResultsContainer: { marginTop: '1rem', maxHeight: '50vh', overflowY: 'auto' },
  resultsInfo: { color: '#333', fontSize: '0.9rem', marginBottom: '1rem' },
  supplierGroup: { marginBottom: '1.5rem', padding: '1rem', background: '#f8f9fa', borderRadius: '8px' },
  supplierHeader: { margin: '0 0 0.75rem', color: '#1a1a2e', fontSize: '1rem' },
  searchResultsTable: { width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' },
  searchTh: { textAlign: 'left', padding: '0.5rem', borderBottom: '2px solid #ddd', fontWeight: '600', background: '#e9ecef' },
  searchThRight: { textAlign: 'right', padding: '0.5rem', borderBottom: '2px solid #ddd', fontWeight: '600', background: '#e9ecef' },
  searchTd: { padding: '0.5rem', borderBottom: '1px solid #eee' },
  searchTdRight: { padding: '0.5rem', borderBottom: '1px solid #eee', textAlign: 'right' },
  currentItemSection: { marginBottom: '1.5rem', padding: '1rem', background: '#e8f4f8', borderRadius: '8px', border: '2px solid #2196f3' },
  currentItemHeader: { margin: '0 0 0.75rem', color: '#1565c0', fontSize: '1rem', fontWeight: '600' },
  currentItemRow: { background: '#fff' },
  invoiceLink: { color: '#1976d2', textDecoration: 'none', fontSize: '0.8rem', padding: '0.2rem 0.5rem', background: '#e3f2fd', borderRadius: '4px' },
  // Error banner styles
  errorBanner: { display: 'flex', gap: '1rem', padding: '1rem 1.5rem', marginBottom: '1.5rem', background: '#fff3cd', border: '2px solid #ffc107', borderRadius: '8px', color: '#856404' },
  errorBannerIcon: { fontSize: '2rem', flexShrink: 0 },
  errorBannerContent: { flex: 1 },
  // Description swap modal styles
  descSwapModal: { background: 'white', padding: '2rem', borderRadius: '12px', maxWidth: '600px', width: '90%', maxHeight: '80vh', overflowY: 'auto' },
  descriptionOption: { padding: '1rem', borderRadius: '8px', background: '#f9f9f9', border: '2px solid #ccc', cursor: 'pointer', transition: 'background 0.2s' },
  modalHint: { color: '#666', fontSize: '0.9rem', margin: 0 },
}
