/**
 * API Service for Kiwi Backend
 *
 * VITE_API_BASE is baked in at build time:
 *   dev:   '' (empty — proxy in vite.config.ts handles /api/)
 *   cloud: '/kiwi' (Caddy strips /kiwi and forwards to nginx, which proxies /api/ → api container)
 */

import axios, { type AxiosInstance } from 'axios'

// API Configuration
const API_BASE_URL = (import.meta.env.VITE_API_BASE ?? '') + '/api/v1'

// Create axios instance
const api: AxiosInstance = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
  timeout: 30000, // 30 seconds
})

// Request interceptor for logging
api.interceptors.request.use(
  (config) => {
    console.log(`[API] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`, {
      params: config.params,
      data: config.data instanceof FormData ? '<FormData>' : config.data,
    })
    return config
  },
  (error) => {
    console.error('[API Request Error]', error)
    return Promise.reject(error)
  }
)

// Response interceptor for error handling
api.interceptors.response.use(
  (response) => {
    console.log(`[API] ✓ ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`)
    return response
  },
  (error) => {
    console.error('[API Error]', {
      message: error.message,
      url: error.config?.url,
      method: error.config?.method?.toUpperCase(),
      status: error.response?.status,
      statusText: error.response?.statusText,
      data: error.response?.data,
      baseURL: error.config?.baseURL,
    })
    return Promise.reject(error)
  }
)

// ========== Types ==========

export interface Product {
  id: string
  barcode: string | null
  name: string
  brand: string | null
  category: string | null
  description: string | null
  image_url: string | null
  nutrition_data: Record<string, any>
  source: string
  tags: Tag[]
}

export interface Tag {
  id: string
  name: string
  slug: string
  description: string | null
  color: string | null
  category: string | null
}

export interface InventoryItem {
  id: number
  product_id: number
  product_name: string | null
  barcode: string | null
  category: string | null
  quantity: number
  unit: string
  location: string
  sublocation: string | null
  purchase_date: string | null
  expiration_date: string | null
  opened_date: string | null
  opened_expiry_date: string | null
  secondary_state: string | null
  secondary_uses: string[] | null
  secondary_warning: string | null
  status: string
  source: string
  notes: string | null
  disposal_reason: string | null
  created_at: string
  updated_at: string
}

export interface BarcodeScanResult {
  barcode: string
  barcode_type: string
  product: Product | null
  inventory_item: InventoryItem | null
  added_to_inventory: boolean
  needs_manual_entry: boolean
  needs_visual_capture: boolean
  message: string
}

export interface LabelCaptureResult {
  barcode: string
  product_name: string | null
  brand: string | null
  serving_size_g: number | null
  calories: number | null
  fat_g: number | null
  saturated_fat_g: number | null
  carbs_g: number | null
  sugar_g: number | null
  fiber_g: number | null
  protein_g: number | null
  sodium_mg: number | null
  ingredient_names: string[]
  allergens: string[]
  confidence: number
  needs_review: boolean
}

export interface LabelConfirmRequest {
  barcode: string
  product_name?: string | null
  brand?: string | null
  serving_size_g?: number | null
  calories?: number | null
  fat_g?: number | null
  saturated_fat_g?: number | null
  carbs_g?: number | null
  sugar_g?: number | null
  fiber_g?: number | null
  protein_g?: number | null
  sodium_mg?: number | null
  ingredient_names?: string[]
  allergens?: string[]
  confidence?: number
  location?: string
  quantity?: number
  auto_add?: boolean
}

export interface BarcodeScanResponse {
  success: boolean
  barcodes_found: number
  results: BarcodeScanResult[]
  message: string
}

export interface InventoryItemUpdate {
  quantity?: number
  unit?: string
  location?: string
  sublocation?: string | null
  purchase_date?: string | null
  expiration_date?: string | null
  status?: string
  notes?: string | null
}

export interface InventoryStats {
  total_items: number
  available_items: number
  expiring_soon: number
  expired_items: number
  locations: Record<string, number>
}

export interface Receipt {
  id: string
  filename: string
  status: string
  metadata: Record<string, any>
  quality_score: number | null
}

export interface ReceiptOCRData {
  id: string
  receipt_id: string
  merchant: {
    name: string | null
    address: string | null
    phone: string | null
  }
  transaction: {
    date: string | null
    time: string | null
    receipt_number: string | null
    register: string | null
    cashier: string | null
  }
  items: Array<{
    name: string
    quantity: number
    unit_price: number | null
    total_price: number
    category: string | null
  }>
  totals: {
    subtotal: number | null
    tax: number | null
    total: number | null
    payment_method: string | null
  }
  confidence: Record<string, number>
  warnings: string[]
  processing_time: number | null
  created_at: string
}

// ========== Inventory API ==========

export const inventoryAPI = {
  /**
   * List all inventory items
   */
  async listItems(params?: {
    location?: string
    item_status?: string
    limit?: number
    offset?: number
  }): Promise<InventoryItem[]> {
    const response = await api.get('/inventory/items', { params })
    return response.data
  },

  /**
   * Get a single inventory item
   */
  async getItem(itemId: string): Promise<InventoryItem> {
    const response = await api.get(`/inventory/items/${itemId}`)
    return response.data
  },

  /**
   * Update an inventory item
   */
  async updateItem(itemId: number, update: InventoryItemUpdate): Promise<InventoryItem> {
    const response = await api.patch(`/inventory/items/${itemId}`, update)
    return response.data
  },

  /**
   * Delete an inventory item
   */
  async deleteItem(itemId: number): Promise<void> {
    await api.delete(`/inventory/items/${itemId}`)
  },

  /**
   * Get inventory statistics
   */
  async getStats(): Promise<InventoryStats> {
    const response = await api.get('/inventory/stats')
    return response.data
  },

  /**
   * Get items expiring soon
   */
  async getExpiring(days: number = 7): Promise<any[]> {
    const response = await api.get(`/inventory/expiring?days=${days}`)
    return response.data
  },

  /**
   * Scan barcode from text
   */
  async scanBarcodeText(
    barcode: string,
    location: string = 'pantry',
    quantity: number = 1.0,
    autoAdd: boolean = true
  ): Promise<BarcodeScanResponse> {
    const response = await api.post('/inventory/scan/text', {
      barcode,
      location,
      quantity,
      auto_add_to_inventory: autoAdd,
    })
    return response.data
  },

  /**
   * Mark item as consumed fully or partially.
   * Pass quantity to decrement; omit to consume all.
   */
  async consumeItem(itemId: number, quantity?: number): Promise<InventoryItem> {
    const body = quantity !== undefined ? { quantity } : undefined
    const response = await api.post(`/inventory/items/${itemId}/consume`, body)
    return response.data
  },

  /**
   * Mark item as discarded (not used, spoiled, etc).
   */
  async discardItem(itemId: number, reason?: string): Promise<InventoryItem> {
    const response = await api.post(`/inventory/items/${itemId}/discard`, { reason: reason ?? null })
    return response.data
  },

  /**
   * Mark item as opened today — starts secondary shelf-life tracking
   */
  async openItem(itemId: number): Promise<InventoryItem> {
    const response = await api.post(`/inventory/items/${itemId}/open`)
    return response.data
  },

  /**
   * Create a new product
   */
  async createProduct(data: {
    name: string
    brand?: string
    source?: string
  }): Promise<Product> {
    const response = await api.post('/inventory/products', data)
    return response.data
  },

  /**
   * Create a new inventory item
   */
  async createItem(data: {
    product_id: string
    quantity: number
    unit?: string
    location: string
    expiration_date?: string
    source?: string
  }): Promise<InventoryItem> {
    const response = await api.post('/inventory/items', data)
    return response.data
  },

  /**
   * Bulk-add items by ingredient name (no barcode required).
   * Idempotent: re-adding an existing product just creates a new inventory entry.
   */
  async bulkAddByName(items: Array<{
    name: string
    quantity?: number
    unit?: string
    location?: string
  }>): Promise<{ added: number; failed: number; results: Array<{ name: string; ok: boolean; item_id?: number; error?: string }> }> {
    const response = await api.post('/inventory/items/bulk-add-by-name', { items })
    return response.data
  },

  /**
   * Scan barcode from image
   */
  async scanBarcodeImage(
    file: File,
    location: string = 'pantry',
    quantity: number = 1.0,
    autoAdd: boolean = true
  ): Promise<any> {
    const formData = new FormData()
    formData.append('file', file)
    formData.append('location', location)
    formData.append('quantity', quantity.toString())
    formData.append('auto_add_to_inventory', autoAdd.toString())

    const response = await api.post('/inventory/scan', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      timeout: 90000, // OFF lookup + LLM expiry prediction can take 30-60s on cold start
    })
    return response.data
  },

  /**
   * Upload a nutrition label photo for an unenriched barcode (paid tier).
   * Returns extracted fields + confidence score for user review.
   */
  async captureLabelPhoto(
    file: File,
    barcode: string
  ): Promise<LabelCaptureResult> {
    const formData = new FormData()
    formData.append('file', file)
    formData.append('barcode', barcode)
    const response = await api.post('/inventory/scan/label-capture', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      timeout: 60000, // vision inference can take ~5–10s
    })
    return response.data
  },

  /**
   * Confirm a user-reviewed label extraction and save to the local cache.
   */
  async confirmLabelCapture(data: LabelConfirmRequest): Promise<{ ok: boolean; product_id?: number; inventory_item_id?: number; message: string }> {
    const response = await api.post('/inventory/scan/label-confirm', data)
    return response.data
  },
}

// ========== Receipts API ==========

export const receiptsAPI = {
  /**
   * List all receipts
   */
  async listReceipts(): Promise<Receipt[]> {
    const response = await api.get('/receipts/')
    return response.data
  },

  /**
   * Get a single receipt
   */
  async getReceipt(receiptId: string): Promise<Receipt> {
    const response = await api.get(`/receipts/${receiptId}`)
    return response.data
  },

  /**
   * Upload a receipt
   */
  async upload(file: File): Promise<Receipt> {
    const formData = new FormData()
    formData.append('file', file)

    const response = await api.post('/receipts/', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    })
    return response.data
  },

  /**
   * Get receipt statistics
   */
  async getStats(): Promise<any> {
    const response = await api.get('/export/stats')
    return response.data
  },

  /**
   * Get OCR data for a receipt
   */
  async getOCRData(receiptId: string): Promise<ReceiptOCRData> {
    const response = await api.get(`/receipts/${receiptId}/ocr/data`)
    return response.data
  },

  /**
   * Get OCR status for a receipt
   */
  async getOCRStatus(receiptId: string): Promise<any> {
    const response = await api.get(`/receipts/${receiptId}/ocr/status`)
    return response.data
  },

  /**
   * Trigger OCR processing
   */
  async triggerOCR(receiptId: string, forceReprocess: boolean = false): Promise<any> {
    const response = await api.post(`/receipts/${receiptId}/ocr/trigger`, {
      force_reprocess: forceReprocess,
    })
    return response.data
  },
}

// ========== Export API ==========

export const exportAPI = {
  /**
   * Get export statistics
   */
  async getStats(): Promise<any> {
    const response = await api.get('/export/stats')
    return response.data
  },

  /**
   * Download inventory CSV
   */
  getInventoryCSVUrl(location?: string, status: string = 'available'): string {
    const params = new URLSearchParams()
    if (location) params.append('location', location)
    params.append('status', status)
    return `${API_BASE_URL}/export/inventory/csv?${params.toString()}`
  },

  /**
   * Download inventory Excel
   */
  getInventoryExcelUrl(location?: string, status: string = 'available'): string {
    const params = new URLSearchParams()
    if (location) params.append('location', location)
    params.append('status', status)
    return `${API_BASE_URL}/export/inventory/excel?${params.toString()}`
  },

  /**
   * Download receipts CSV
   */
  getReceiptsCSVUrl(): string {
    return `${API_BASE_URL}/export/csv`
  },

  /**
   * Download receipts Excel
   */
  getReceiptsExcelUrl(): string {
    return `${API_BASE_URL}/export/excel`
  },
}

// ========== Recipes & Settings Types ==========

export interface SwapCandidate {
  original_name: string
  substitute_name: string
  constraint_label: string
  explanation: string
  compensation_hints: Record<string, string>[]
}

export interface NutritionPanel {
  calories: number | null
  fat_g: number | null
  protein_g: number | null
  carbs_g: number | null
  fiber_g: number | null
  sugar_g: number | null
  sodium_mg: number | null
  servings: number | null
  estimated: boolean
}

export interface RecipeSuggestion {
  id: number
  title: string
  match_count: number
  element_coverage: Record<string, number>
  swap_candidates: SwapCandidate[]
  matched_ingredients: string[]
  missing_ingredients: string[]
  directions: string[]
  prep_notes: string[]
  notes: string
  level: number
  is_wildcard: boolean
  nutrition: NutritionPanel | null
  source_url: string | null
  complexity: 'easy' | 'moderate' | 'involved' | null
  estimated_time_min: number | null
  time_effort: TimeEffortProfile | null
}

export interface NutritionFilters {
  max_calories: number | null
  max_sugar_g: number | null
  max_carbs_g: number | null
  max_sodium_mg: number | null
}

export interface GroceryLink {
  ingredient: string
  retailer: string
  url: string
}

export interface RecipeResult {
  suggestions: RecipeSuggestion[]
  element_gaps: string[]
  grocery_list: string[]
  grocery_links: GroceryLink[]
  rate_limited: boolean
  rate_limit_count: number
}

export interface StreamTokenResponse {
  stream_url: string
  token: string
  expires_in_s: number
}

export type RecipeJobStatusValue = 'queued' | 'running' | 'done' | 'failed'

export interface RecipeJobStatus {
  job_id: string
  status: RecipeJobStatusValue
  result: RecipeResult | null
  error: string | null
}

export interface RecipeRequest {
  pantry_items: string[]
  secondary_pantry_items: Record<string, string>
  level: number
  constraints: string[]
  allergies: string[]
  expiry_first: boolean
  hard_day_mode: boolean
  max_missing: number | null
  style_id: string | null
  category: string | null
  wildcard_confirmed: boolean
  nutrition_filters: NutritionFilters
  excluded_ids: number[]
  exclude_ingredients: string[]
  shopping_mode: boolean
  pantry_match_only: boolean
  complexity_filter: string | null
  max_time_min: number | null
  max_total_min: number | null
  max_active_min: number | null
}

export interface Staple {
  slug: string
  name: string
  category: string
  dietary_tags: string[]
}

// ── Build Your Own types ──────────────────────────────────────────────────

export interface AssemblyRoleOut {
  display: string
  required: boolean
  keywords: string[]
  hint: string
}

export interface AssemblyTemplateOut {
  id: string
  title: string
  icon: string
  descriptor: string
  role_sequence: AssemblyRoleOut[]
}

export interface RoleCandidateItem {
  name: string
  in_pantry: boolean
  tags: string[]
}

export interface RoleCandidatesResponse {
  compatible: RoleCandidateItem[]
  other: RoleCandidateItem[]
  available_tags: string[]
}

export interface BuildRequest {
  template_id: string
  role_overrides: Record<string, string>
}

// ── Ask/RAG types ──────────────────────────────────────────────────────────

export interface AskRecipeHit {
  id: number
  title: string
  match_pct: number | null
  category: string | null
}

export interface AskResponse {
  answer: string | null
  recipes: AskRecipeHit[]
  tier: string
}

// ========== Recipes API ==========

export const recipesAPI = {
  async suggest(req: RecipeRequest): Promise<RecipeResult> {
    // Allow up to 120s — cf-orch model cold-start can take 60+ seconds on first request
    const response = await api.post('/recipes/suggest', req, { timeout: 120000 })
    return response.data
  },

  /** Submit an async job for L3/L4 generation. Returns job_id + initial status. */
  async suggestAsync(req: RecipeRequest): Promise<{ job_id: string; status: string }> {
    const response = await api.post('/recipes/suggest', req, { params: { async: 'true' }, timeout: 15000 })
    return response.data
  },

  /** Poll an async job. Returns the full status including result once done. */
  async pollJob(jobId: string): Promise<RecipeJobStatus> {
    const response = await api.get(`/recipes/jobs/${jobId}`, { timeout: 10000 })
    return response.data
  },
  async getRecipe(id: number): Promise<RecipeSuggestion> {
    const response = await api.get(`/recipes/${id}`)
    return response.data
  },
  async getLeftovers(id: number): Promise<{ fridge_days: number; freeze_days: number | null; freeze_by_day: number | null; storage_advice: string }> {
    const response = await api.post(`/recipes/${id}/leftovers`)
    return response.data
  },
  async listStaples(dietary?: string): Promise<Staple[]> {
    const response = await api.get('/staples/', { params: dietary ? { dietary } : undefined })
    return response.data
  },
  async getTemplates(): Promise<AssemblyTemplateOut[]> {
    const response = await api.get('/recipes/templates')
    return response.data
  },
  async getRoleCandidates(
    templateId: string,
    role: string,
    priorPicks: string[] = [],
  ): Promise<RoleCandidatesResponse> {
    const response = await api.get('/recipes/template-candidates', {
      params: {
        template_id: templateId,
        role,
        prior_picks: priorPicks.join(','),
      },
    })
    return response.data
  },
  async buildRecipe(req: BuildRequest): Promise<RecipeSuggestion> {
    const response = await api.post('/recipes/build', req)
    return response.data
  },

  /** Issue a one-time stream token for LLM recipe generation (Paid tier / BYOK only). */
  async getRecipeStreamToken(params: {
    level: 3 | 4
    wildcard_confirmed?: boolean
  }): Promise<StreamTokenResponse> {
    const response = await api.post('/recipes/stream-token', {
      level: params.level,
      wildcard_confirmed: params.wildcard_confirmed ?? false,
    })
    return response.data
  },

  /** Natural-language recipe search with optional LLM synthesis (Paid tier). */
  async ask(question: string, pantryItems: string[] = []): Promise<AskResponse> {
    const response = await api.post('/recipes/ask', { question, pantry_items: pantryItems }, { timeout: 30000 })
    return response.data
  },

  /** Stream a recipe via native SSE (Ollama fallback). Calls callbacks as tokens arrive. */
  async suggestRecipeStream(
    req: RecipeRequest,
    onChunk: (chunk: string) => void,
    onDone: () => void,
    onError: (err: string) => void,
  ): Promise<void> {
    const baseUrl = (api.defaults.baseURL ?? '') as string
    let response: Response
    try {
      response = await fetch(`${baseUrl}/recipes/suggest?stream=true`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(req),
      })
    } catch (err: unknown) {
      onError(err instanceof Error ? err.message : 'Network error')
      return
    }

    if (!response.ok) {
      onError(`HTTP ${response.status}`)
      return
    }

    const reader = response.body?.getReader()
    if (!reader) { onError('No response body'); return }

    const decoder = new TextDecoder()
    let buffer = ''
    while (true) {
      const { done, value } = await reader.read()
      if (done) { onDone(); break }
      buffer += decoder.decode(value, { stream: true })
      const parts = buffer.split('\n\n')
      buffer = parts.pop() ?? ''
      for (const part of parts) {
        if (!part.startsWith('data: ')) continue
        try {
          const data = JSON.parse(part.slice(6))
          if (data.done) { onDone(); return }
          else if (data.error) { onError(data.error); return }
          else if (data.chunk) { onChunk(data.chunk) }
        } catch { /* ignore malformed events */ }
      }
    }
  },
}

// ========== Settings API ==========

export const settingsAPI = {
  async getSetting(key: string): Promise<string | null> {
    try {
      const response = await api.get(`/settings/${key}`)
      return response.data.value
    } catch {
      return null
    }
  },
  async setSetting(key: string, value: string): Promise<void> {
    await api.put(`/settings/${key}`, { value })
  },
}

// ========== Household Types ==========

export interface HouseholdMember {
  user_id: string
  joined_at: string
  is_owner: boolean
}

export interface HouseholdStatus {
  in_household: boolean
  household_id: string | null
  is_owner: boolean
  members: HouseholdMember[]
  max_seats: number
}

export interface HouseholdInvite {
  invite_url: string
  token: string
  expires_at: string
}

// ========== Household API ==========

export const householdAPI = {
  async create(): Promise<{ household_id: string; message: string }> {
    const response = await api.post('/household/create')
    return response.data
  },
  async status(): Promise<HouseholdStatus> {
    const response = await api.get('/household/status')
    return response.data
  },
  async invite(): Promise<HouseholdInvite> {
    const response = await api.post('/household/invite')
    return response.data
  },
  async accept(householdId: string, token: string): Promise<{ message: string; household_id: string }> {
    const response = await api.post('/household/accept', { household_id: householdId, token })
    return response.data
  },
  async leave(): Promise<{ message: string }> {
    const response = await api.post('/household/leave')
    return response.data
  },
  async removeMember(userId: string): Promise<{ message: string }> {
    const response = await api.post('/household/remove-member', { user_id: userId })
    return response.data
  },
}

// ========== Saved Recipes Types ==========

export interface SavedRecipe {
  id: number
  recipe_id: number
  title: string
  saved_at: string
  notes: string | null
  rating: number | null
  style_tags: string[]
  collection_ids: number[]
}

export interface RecipeCollection {
  id: number
  name: string
  description: string | null
  member_count: number
  created_at: string
}

// ========== Saved Recipes API ==========

export const savedRecipesAPI = {
  async save(recipe_id: number, notes?: string, rating?: number): Promise<SavedRecipe> {
    const response = await api.post('/recipes/saved', { recipe_id, notes, rating })
    return response.data
  },
  async unsave(recipe_id: number): Promise<void> {
    await api.delete(`/recipes/saved/${recipe_id}`)
  },
  async update(recipe_id: number, data: { notes?: string | null; rating?: number | null; style_tags?: string[] }): Promise<SavedRecipe> {
    const response = await api.patch(`/recipes/saved/${recipe_id}`, data)
    return response.data
  },
  async list(params?: { sort_by?: string; collection_id?: number }): Promise<SavedRecipe[]> {
    const response = await api.get('/recipes/saved', { params })
    return response.data
  },
  async listCollections(): Promise<RecipeCollection[]> {
    const response = await api.get('/recipes/saved/collections')
    return response.data
  },
  async createCollection(name: string, description?: string): Promise<RecipeCollection> {
    const response = await api.post('/recipes/saved/collections', { name, description })
    return response.data
  },
  async deleteCollection(id: number): Promise<void> {
    await api.delete(`/recipes/saved/collections/${id}`)
  },
  async addToCollection(collection_id: number, saved_recipe_id: number): Promise<void> {
    await api.post(`/recipes/saved/collections/${collection_id}/members`, { saved_recipe_id })
  },
  async removeFromCollection(collection_id: number, saved_recipe_id: number): Promise<void> {
    await api.delete(`/recipes/saved/collections/${collection_id}/members/${saved_recipe_id}`)
  },
  async classifyStyle(recipe_id: number): Promise<string[]> {
    const response = await api.post(`/recipes/saved/${recipe_id}/classify-style`)
    return response.data.suggested_tags
  },
}

// --- Meal Plan types ---

export interface MealPlanSlot {
  id: number
  plan_id: number
  day_of_week: number       // 0 = Monday
  meal_type: string
  recipe_id: number | null
  recipe_title: string | null
  servings: number
  custom_label: string | null
}

export interface MealPlan {
  id: number
  week_start: string        // ISO date, e.g. "2026-04-13"
  meal_types: string[]
  slots: MealPlanSlot[]
  created_at: string
}

export interface RetailerLink {
  retailer: string
  label: string
  url: string
}

export interface GapItem {
  ingredient_name: string
  needed_raw: string | null
  have_quantity: number | null
  have_unit: string | null
  covered: boolean
  retailer_links: RetailerLink[]
}

export interface ShoppingList {
  plan_id: number
  gap_items: GapItem[]
  covered_items: GapItem[]
  disclosure: string | null
}

export interface PrepTask {
  id: number
  recipe_id: number | null
  task_label: string
  duration_minutes: number | null
  sequence_order: number
  equipment: string | null
  is_parallel: boolean
  notes: string | null
  user_edited: boolean
}

export interface PrepSession {
  id: number
  plan_id: number
  scheduled_date: string
  status: 'draft' | 'reviewed' | 'done'
  tasks: PrepTask[]
}

// --- Meal Plan API ---

export const mealPlanAPI = {
  async list(): Promise<MealPlan[]> {
    const resp = await api.get<MealPlan[]>('/meal-plans/')
    return resp.data
  },

  async create(weekStart: string, mealTypes: string[]): Promise<MealPlan> {
    const resp = await api.post<MealPlan>('/meal-plans/', { week_start: weekStart, meal_types: mealTypes })
    return resp.data
  },

  async get(planId: number): Promise<MealPlan> {
    const resp = await api.get<MealPlan>(`/meal-plans/${planId}`)
    return resp.data
  },

  async updateMealTypes(planId: number, mealTypes: string[]): Promise<MealPlan> {
    const resp = await api.patch<MealPlan>(`/meal-plans/${planId}`, { meal_types: mealTypes })
    return resp.data
  },

  async upsertSlot(planId: number, dayOfWeek: number, mealType: string, data: { recipe_id?: number | null; servings?: number; custom_label?: string | null }): Promise<MealPlanSlot> {
    const resp = await api.put<MealPlanSlot>(`/meal-plans/${planId}/slots/${dayOfWeek}/${mealType}`, data)
    return resp.data
  },

  async deleteSlot(planId: number, slotId: number): Promise<void> {
    await api.delete(`/meal-plans/${planId}/slots/${slotId}`)
  },

  async getShoppingList(planId: number): Promise<ShoppingList> {
    const resp = await api.get<ShoppingList>(`/meal-plans/${planId}/shopping-list`)
    return resp.data
  },

  async getPrepSession(planId: number): Promise<PrepSession> {
    const resp = await api.get<PrepSession>(`/meal-plans/${planId}/prep-session`)
    return resp.data
  },

  async updatePrepTask(planId: number, taskId: number, data: Partial<Pick<PrepTask, 'duration_minutes' | 'sequence_order' | 'notes' | 'equipment'>>): Promise<PrepTask> {
    const resp = await api.patch<PrepTask>(`/meal-plans/${planId}/prep-session/tasks/${taskId}`, data)
    return resp.data
  },
}

// ========== Orch Usage Types ==========

export interface OrchUsage {
  calls_used: number
  topup_calls: number
  calls_total: number
  period_start: string  // ISO date YYYY-MM-DD
  resets_on: string     // ISO date YYYY-MM-DD
}

// ========== Browser Types ==========

export interface BrowserDomain {
  id: string
  label: string
}

export interface BrowserCategory {
  category: string
  recipe_count: number
  has_subcategories: boolean
}

export interface BrowserSubcategory {
  subcategory: string
  recipe_count: number
}

// ── Time & Effort types ───────────────────────────────────────────────────

export interface StepAnalysis {
  is_passive: boolean
  detected_minutes: number | null
}

export interface TimeEffortProfile {
  active_min: number
  passive_min: number
  total_min: number
  effort_label: 'quick' | 'moderate' | 'involved'
  equipment: string[]
  step_analyses: StepAnalysis[]
}

export interface BrowserRecipe {
  id: number
  title: string
  category: string | null
  match_pct: number | null
  active_min: number | null
  passive_min: number | null
}

export interface BrowserResult {
  recipes: BrowserRecipe[]
  total: number
  page: number
}

// ========== Browser API ==========

export const browserAPI = {
  async listDomains(): Promise<BrowserDomain[]> {
    const response = await api.get('/recipes/browse/domains')
    return response.data
  },
  async listCategories(domain: string): Promise<BrowserCategory[]> {
    const response = await api.get(`/recipes/browse/${domain}`)
    return response.data
  },
  async listSubcategories(domain: string, category: string): Promise<BrowserSubcategory[]> {
    const response = await api.get(
      `/recipes/browse/${domain}/${encodeURIComponent(category)}/subcategories`
    )
    return response.data
  },
  async browse(domain: string, category: string, params?: {
    page?: number
    page_size?: number
    pantry_items?: string
    subcategory?: string
    q?: string
    sort?: string
    required_ingredient?: string
  }): Promise<BrowserResult> {
    const response = await api.get(`/recipes/browse/${domain}/${encodeURIComponent(category)}`, { params })
    return response.data
  },

  async submitRecipeTag(body: {
    recipe_id: number
    domain: string
    category: string
    subcategory: string | null
    pseudonym: string
  }): Promise<void> {
    await api.post('/recipes/community-tags', body)
  },

  async upvoteRecipeTag(tagId: number, pseudonym: string): Promise<void> {
    await api.post(`/recipes/community-tags/${tagId}/upvote`, null, { params: { pseudonym } })
  },

  async listRecipeTags(recipeId: number): Promise<Array<{
    id: number; domain: string; category: string; subcategory: string | null;
    pseudonym: string; upvotes: number; accepted: boolean
  }>> {
    const response = await api.get(`/recipes/community-tags/${recipeId}`)
    return response.data
  },
}

// ── Shopping List ─────────────────────────────────────────────────────────────

export interface GroceryLink {
  ingredient: string
  retailer: string
  url: string
}

export interface ShoppingItem {
  id: number
  name: string
  quantity: number | null
  unit: string | null
  category: string | null
  checked: boolean
  notes: string | null
  source: string
  recipe_id: number | null
  sort_order: number
  created_at: string
  updated_at: string
  grocery_links: GroceryLink[]
}

export interface ShoppingItemCreate {
  name: string
  quantity?: number
  unit?: string
  category?: string
  notes?: string
  source?: string
  recipe_id?: number
  sort_order?: number
}

export interface ShoppingItemUpdate {
  name?: string
  quantity?: number
  unit?: string
  category?: string
  checked?: boolean
  notes?: string
  sort_order?: number
}

export const shoppingAPI = {
  list: (includeChecked = true) =>
    api.get<ShoppingItem[]>('/shopping', { params: { include_checked: includeChecked } }).then(r => r.data),

  add: (item: ShoppingItemCreate) =>
    api.post<ShoppingItem>('/shopping', item).then(r => r.data),

  addFromRecipe: (recipeId: number, includeCovered = false) =>
    api.post<ShoppingItem[]>('/shopping/from-recipe', { recipe_id: recipeId, include_covered: includeCovered }).then(r => r.data),

  update: (id: number, update: ShoppingItemUpdate) =>
    api.patch<ShoppingItem>(`/shopping/${id}`, update).then(r => r.data),

  remove: (id: number) =>
    api.delete(`/shopping/${id}`),

  clearChecked: () =>
    api.delete('/shopping/checked'),

  clearAll: () =>
    api.delete('/shopping/all'),

  confirmPurchase: (id: number, location = 'pantry', quantity?: number, unit?: string) =>
    api.post(`/shopping/${id}/confirm`, { location, quantity, unit }).then(r => r.data),
}

// ── Orch Usage ────────────────────────────────────────────────────────────────

export async function getOrchUsage(): Promise<OrchUsage | null> {
  const resp = await api.get<OrchUsage | null>('/orch-usage')
  return resp.data
}

// ── Session Bootstrap ─────────────────────────────────────────────────────────

export interface SessionInfo {
  auth: 'local' | 'anon' | 'authed'
  tier: string
  has_byok: boolean
}

/** Call once on app load. Logs auth= + tier= server-side for analytics. */
export async function bootstrapSession(): Promise<SessionInfo | null> {
  try {
    const resp = await api.get<SessionInfo>('/session/bootstrap')
    return resp.data
  } catch {
    return null
  }
}

// ========== Sensory Preferences Types ==========

export type TextureTag = 'mushy' | 'slimy' | 'crunchy' | 'chewy' | 'creamy' | 'chunky'
export type SmellLevel = 'mild' | 'aromatic' | 'pungent' | 'fermented' | null
export type NoiseLevel = 'quiet' | 'moderate' | 'loud' | 'very_loud' | null

export interface SensoryPreferences {
  avoid_textures: TextureTag[]
  max_smell: SmellLevel
  max_noise: NoiseLevel
}

export const DEFAULT_SENSORY_PREFERENCES: SensoryPreferences = {
  avoid_textures: [],
  max_smell: null,
  max_noise: null,
}

// ── Recipe Scanner (kiwi#9) ───────────────────────────────────────────────────

export interface ScannedIngredient {
  name: string
  qty: string | null
  unit: string | null
  raw: string | null
  in_pantry: boolean
}

export interface ScannedRecipe {
  title: string | null
  subtitle: string | null
  servings: string | null
  cook_time: string | null
  source_note: string | null
  ingredients: ScannedIngredient[]
  steps: string[]
  notes: string | null
  tags: string[]
  pantry_match_pct: number
  confidence: 'high' | 'medium' | 'low'
  warnings: string[]
}

export interface UserRecipe {
  id: number
  title: string
  subtitle: string | null
  servings: string | null
  cook_time: string | null
  source_note: string | null
  ingredients: ScannedIngredient[]
  steps: string[]
  notes: string | null
  tags: string[]
  source: string
  pantry_match_pct: number | null
  created_at: string
}

export const recipeScanAPI = {
  /** Scan 1-4 recipe photos. Returns structured recipe for review (not saved). */
  scan(files: File[]): Promise<ScannedRecipe> {
    const form = new FormData()
    files.forEach((f) => form.append('files', f))
    return api.post('/recipes/scan', form, {
      headers: { 'Content-Type': 'multipart/form-data' },
      timeout: 120_000, // VLM can be slow on first call
    }).then((r) => r.data)
  },

  /** Scan recipe photos with live SSE progress events.
   *
   * Calls onProgress(status, message) for each intermediate event
   * ("allocating", "scanning", "structuring"), then resolves with the final
   * ScannedRecipe on success. Rejects on error or timeout.
   */
  async scanStream(
    files: File[],
    onProgress: (status: string, message: string) => void,
  ): Promise<ScannedRecipe> {
    const form = new FormData()
    files.forEach((f) => form.append('files', f))

    const response = await fetch(`${API_BASE_URL}/recipes/scan/stream`, {
      method: 'POST',
      body: form,
    })

    if (!response.ok || !response.body) {
      let detail = ''
      try { detail = await response.text() } catch (_) { /* ignore */ }
      throw new Error(detail || `Scan failed (${response.status})`)
    }

    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    let buffer = ''

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      buffer += decoder.decode(value, { stream: true })
      const lines = buffer.split('\n')
      buffer = lines.pop() ?? ''

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue
        let data: Record<string, unknown>
        try { data = JSON.parse(line.slice(6)) } catch { continue }

        if (data.status === 'done') return data.recipe as ScannedRecipe
        if (data.status === 'error') throw new Error((data.message as string) || 'Scan failed')
        onProgress(data.status as string, data.message as string)
      }
    }

    throw new Error('Stream ended without a result')
  },

  /** Save a reviewed/edited scanned recipe to user_recipes. */
  saveScanned(recipe: Omit<ScannedRecipe, 'pantry_match_pct' | 'confidence' | 'warnings'> & { source?: string }): Promise<UserRecipe> {
    return api.post('/recipes/scan/save', recipe).then((r) => r.data)
  },

  /** List all user-created recipes (scan + manual). */
  listUserRecipes(): Promise<UserRecipe[]> {
    return api.get('/recipes/user').then((r) => r.data)
  },

  /** Get a single user recipe by ID. */
  getUserRecipe(id: number): Promise<UserRecipe> {
    return api.get(`/recipes/user/${id}`).then((r) => r.data)
  },

  /** Delete a user recipe. */
  deleteUserRecipe(id: number): Promise<void> {
    return api.delete(`/recipes/user/${id}`).then(() => undefined)
  },
}

export default api
